AngularJS双向数据绑定原理之$watch、$apply和$digest的应用


Posted in Javascript onJanuary 30, 2018

引子

这篇文章是写给AngularJS新手的,如果你已经对AngularJS的双向数据绑定有了深入的了解,直接去阅读源代码好了。

背景

AngularJS开发者都想知道双向数据绑定是怎么实现的。与data-binding相关的术语琳琅满目: $watch,$apply,$digest,dirty-checking等等它们是如何工作的呢?让我们从头开始讲起吧

AngularJS 的双向数据绑定是被浏览器逼的

浏览器看上去很美,其实在数据交互这块儿,由于浏览器的“不作为”,导致浏览器的数据刷新成为一个难题。具体来说,浏览器可以很容易地监听一个事件,比如:用户点击一个按钮,或者在输入框里输入东西,为此还提供了事件回调函数的API,事件的回调函数就会在javascript解释器里执行;但反过来就没这么简单了,如果来自后台的数据发生了变化,需要通知给浏览器,让浏览器刷新,浏览器并没有提供这样的数据交互机制,对于开发者来说,这是一个难以逾越的障碍,怎么办呢? AngularJS出现了,它通过$scope 很好地实现了双向数据绑定,其背后的原理就是$watch,$apply,$digest,dirty-checking

$watch 队列($watch list)

从字面上看,watch 是观察的意思。 每次绑定一些东西到浏览器上时,就会往$watch队列里插入一条$watch。想象一下$watch就是那个可以检测它监视的model里时候有变化的东西。例如你有如下的代码

User: <input type="text" ng-model="user" />
Password: <input type="password" ng-model="pass" />

这里有个$scope.user,它被绑定在了第一个输入框上,还有个$scope.pass,它被绑定在了第二个输入框上;然后在$watch list里面加入两个$watch:

创建一个 controllers.js 文件,代码如下:

app.controller('MainCtrl', function($scope) {
 $scope.foo = "Foo";
 $scope.world = "World";
});

对应的html 文件, index.html 代码如下:

Hello, {{ World }}

这里,即便在$scope上添加了两个东西,但是只有一个绑定在了UI上,因此只生成了一个$watch. 再看下面的例子:

controllers.js

app.controller('MainCtrl', function($scope) {
 $scope.people = [...];
});

对应的html文件 index.html

<ul>
 <li ng-repeat="person in people">
   {{person.name}} - {{person.age}}
 </li>
</ul>

这样看来,又生成了多个$watch。每个person有两个(一个name,一个age),然后ng-repeat是一个循环,因此10个person一共是(2 * 10) +1,也就是说有21个$watch。 因此,每一个绑定到了浏览器上的数据都会生成一个$watch。对,那这写$watch是什么时候生成的呢? 先回顾下AngularJS的加载原理

AngularJS的加载原理:

AngularJS的模板加载分为编译(compile)和链接(linking)两个阶段,在linking阶段,AngularJS解释器会寻找每个directive,然后生成每个需要的$watch。对了,$watch就是在这个阶段生成的。

接下来,开始用到 $digest了

$digest 循环

从字面上看,digest是 “消化”的意思,总感觉这个名字怪怪的,跟不可思议的是 dirty-checking, 字面意思“脏检查”,还是不翻译为好。原作者的本意肯定不是这个意思,只可意会不可言传!

$digest 是一个循环,它在循环做什么呢? $digest 在遍历我们的$watch。 $digest 一个个地询问$watch —— “嗨,你观察的数据发生变化了没?”

这个遍历就是所谓的dirty-checking。既然所有的$watch都检查完了,那就要问了:有没有$watch更新过?如果有至少一个更新过,这个循环就会再次触发,直到所有的$watch都没有变化。这样就能够保证每个model都已经不会再变化。记住如果循环超过10次的话,它将会抛出一个异常,以免出现无限循环。 当$digest循环结束时,DOM相应地变化。

看段代码,例如: controllers.js

app.controller('MainCtrl', function() {
 $scope.name = "Foo";
 $scope.changeFoo = function() {
   $scope.name = "Bar";
 }
});

对应的html文件,index.html

{{ name }}
<button ng-click="changeFoo()">Change the name</button>

这里只有一个$watch,因为ng-click不生成$watch(函数是不会变的)。

$digest 执行的流程是:

  1. 在浏览器按下按钮;
  2. 浏览器接收到一个事件,进入angular context。
  3. $digest循环开始执行,查询每个$watch是否变化。
  4. 由于监视$scope.name的$watch报告了变化,它会强制再执行一次$digest循环。
  5. 新的$digest循环没有检测到变化,此时浏览器拿回控制权,更新与$scope.name新值相应部分的DOM。

从中可以看出AngularJS的一个明显的不足:每一个进入angular context的事件都会执行一个$digest循环,哪怕仅仅是输入一个字母,$digest 都会遍历整个页面的所有$watch。

$apply 的应用

Angular context 是整个Angular的上下文,也可以把它理解为Angular容器,那么,是谁来决定哪些事件可以进入 Angular Context,哪些事件又不能进入呢? 其控制器在 $apply手上。

如果当事件触发时,调用$apply,它会进入angular context,如果没有调用就不会进入。你可能会问:刚才的例子并没有调用$apply,这是怎么回事呢?原来,是Angular背后替你做了。当点击带有ng-click的元素时,事件就会被封装到一个$apply调用中。如果有一个ng-model="foo"的输入框,当输入一个字母 f 时,事件就会这样调用,$apply("foo = 'f';")。

$apply的应用场景

$apply是$scope的一个函数,调用它会强制一次$digest循环。如果当前正在执行$apply循环,则会抛出一个异常。

如果浏览器上数据没有及时刷新,可以通过调用$scope.$apply() 方法,强行刷新一遍。

通过 $watch 监控自己的$scope

<!DOCTYPE html>
<html ng-app="demoApp">
<head>
 <title>test</title>
 <!-- Vendor libraries -->
  <script src="lib/jquery-v1.11.1.js"></script>
  <script src="lib/angular-v1.2.22.js"></script>
  <script src="lib/angular-route-v1.2.22.js"></script>
</head>
<body> 
 <div ng-controller="MainCtrl" >
  <input ng-model="name" />
  Name updated: {{updated}} times.
 </div> 
 <script >
  var demoApp = angular.module('demoApp',[]); 
  demoApp.controller('MainCtrl', function($scope) {
  $scope.name = "Angular";
  $scope.updated = -1;
  $scope.$watch('name', function() {
  $scope.updated++;
 });
});
 </script>
 </body>
</html>

代码说明:

当controller 执行到 $watch时,它会立即调用一次,所以把updated的值设为 -1 。 上输入框中输入字符发生变化时,你会看到 updated 的值随之变化,而且能显示变化的次数。

AngularJS双向数据绑定原理之$watch、$apply和$digest的应用

$watch 检测到的数据变化

小结

我们对 AngularJS的双向数据绑定有了一个初步的认识,对于AngularJS来说,表面上看操作DOM很简单,其实背后有 $watch、$digest 、 $apply 三者在默默地起着作用。这个遍历检查数据是否发生变化的过程,称之为:dirty-checking。 当你了解了这个过程后,你会对它嗤之以鼻,感觉这种方法好low 哦。 确实,如果一个DOM中有 2000- 3000个 watch,页面的渲染速度将会大打折扣。

这个渲染的性能问题怎么解决呢?随着ECMAScript6的到来,Angular 2 通过Object.observe 极大地改善$digest循环的速度。或许,这就是为什么 Angular 团队迫不及待地推出 Angular 2 的原因吧。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery实现div阴影效果示例代码
Sep 16 Javascript
基于jquery异步传输json数据格式实例代码
Nov 23 Javascript
JsRender for object语法简介
Oct 31 Javascript
jquery禁止回车触发表单提交
Dec 12 Javascript
JS+CSS实现带关闭按钮DIV弹出窗口的方法
Feb 27 Javascript
jQuery动画效果实现图片无缝连续滚动
Jan 12 Javascript
在页面中输出当前客户端时间javascript实例代码
Mar 02 Javascript
BootStrap 附加导航组件
Jul 22 Javascript
原生Javascript插件开发实践
Jan 18 Javascript
vue.js  父向子组件传参的实例代码
Oct 29 Javascript
在小程序开发中使用npm的方法
Oct 17 Javascript
解决echarts 一条柱状图显示两个值,类似进度条的问题
Jul 20 Javascript
微信小程序数据存储与取值详解
Jan 30 #Javascript
Vue精简版风格概述
Jan 30 #Javascript
vue自定义全局组件(自定义插件)的用法
Jan 30 #Javascript
vue2.0之多页面的开发的示例
Jan 30 #Javascript
vue-cli实现多页面多路由的示例代码
Jan 30 #Javascript
jQuery与vue实现拖动验证码功能
Jan 30 #jQuery
5 种JavaScript编码规范
Jan 30 #Javascript
You might like
一个高ai的分页函数和一个url函数
2006/10/09 PHP
PHP中strtr字符串替换用法详解
2014/11/26 PHP
PHP设计模式之适配器模式定义与用法详解
2018/04/03 PHP
PHP使用zlib扩展实现GZIP压缩输出的方法详解
2018/04/09 PHP
使用Git实现Laravel项目的自动化部署
2019/11/24 PHP
JS无限树状列表实现代码
2011/01/11 Javascript
Javascript 构造函数详解
2014/10/22 Javascript
JavaScript中的函数嵌套使用
2015/06/04 Javascript
基于JavaScript如何实现私有成员的语法特征及私有成员的实现方式
2015/10/28 Javascript
实例详解jQuery Mockjax 插件模拟 Ajax 请求
2016/01/12 Javascript
Vue.js第一天学习笔记(数据的双向绑定、常用指令)
2016/12/01 Javascript
AngularJS 文件上传控件 ng-file-upload详解
2017/01/13 Javascript
Angular2搜索和重置按钮过场动画
2017/05/24 Javascript
JQuery 获取Dom元素的实例讲解
2017/07/08 jQuery
BootStrap点击保存后实现模态框自动关闭的思路(模态框)
2017/09/26 Javascript
实例分析编写vue组件方法
2019/02/12 Javascript
autojs 蚂蚁森林能量自动拾取即给指定好友浇水的实现方法
2020/05/03 Javascript
js实现轮播图特效
2020/05/28 Javascript
[01:08:29]DOTA2-DPC中国联赛定级赛 RNG vs Aster BO3第一场 1月9日
2021/03/11 DOTA
Python编程中用close()方法关闭文件的教程
2015/05/24 Python
Python的socket模块源码中的一些实现要点分析
2016/06/06 Python
对python 判断数字是否小于0的方法详解
2019/01/26 Python
Python接口自动化判断元素原理解析
2020/02/24 Python
Python递归调用实现数字累加的代码
2020/02/25 Python
python 两种方法删除空文件夹
2020/09/29 Python
html5 利用canvas手写签名并保存的实现方法
2018/07/12 HTML / CSS
The North Face北面英国官网:美国著名户外品牌
2017/12/13 全球购物
比利时的在线灯具店:Lampen24.be
2019/07/01 全球购物
澳大利亚礼品篮网站:Macarthur Baskets
2019/10/14 全球购物
西部世纪面试题
2014/12/05 面试题
法学院方阵解说词
2014/01/29 职场文书
《陶罐和铁罐》教学反思
2014/02/19 职场文书
广播体操口号
2014/06/18 职场文书
2015年家长学校工作总结
2015/04/22 职场文书
2015年中学校长工作总结
2015/05/19 职场文书
2015暑期工社会实践报告
2015/07/13 职场文书