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 相关文章推荐
JS中字符问题(二进制/十进制/十六进制及ASCII码之间的转换)
Nov 03 Javascript
重载toString实现JS HashMap分析
Mar 13 Javascript
javaScript 计算两个日期的天数相差(示例代码)
Dec 27 Javascript
jquery+ajax验证不通过也提交表单问题处理
Dec 12 Javascript
jQuery简单实现禁用右键菜单
Mar 10 Javascript
jquery siblings获取同辈元素用法实例分析
Jul 25 Javascript
BootStrap table使用方法分析
Nov 08 Javascript
详解Vue单元测试case写法
May 24 Javascript
socket在egg中的使用实例代码详解
May 30 Javascript
JavaScript装箱及拆箱boxing及unBoxing用法解析
Jun 15 Javascript
OpenLayer学习之自定义测量控件
Sep 28 Javascript
解决vue初始化项目一直停在downloading template的问题
Nov 09 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
如何使用PHP获取网络上文件
2006/10/09 PHP
PHP写的资源下载防盗链类分享
2014/05/12 PHP
Yii配置文件用法详解
2014/12/04 PHP
php中关于换行的实例写法
2019/09/26 PHP
来自qq的javascript面试题
2010/07/24 Javascript
JavaScript的parseInt 取整使用
2011/05/09 Javascript
一个简单的JS鼠标悬停特效具体方法
2013/06/17 Javascript
浅谈javascript中for in 和 for each in的区别
2015/04/23 Javascript
JS模式之单例模式基本用法
2015/06/30 Javascript
详解jquery事件delegate()的使用方法
2016/01/25 Javascript
angularjs表格ng-table使用备忘录
2016/03/09 Javascript
JS实现title标题栏文字不间断滚动显示效果
2016/09/07 Javascript
js实现3D图片环展示效果
2017/03/09 Javascript
Vue.js中组件中的slot实例详解
2017/07/17 Javascript
jQuery Ajax 实现分页 kkpager插件实例代码
2017/08/10 jQuery
webpack构建的详细流程探底
2018/01/08 Javascript
mui框架 页面无法滚动的解决方法(推荐)
2018/01/25 Javascript
Node.js爬取豆瓣数据实例分析
2018/03/05 Javascript
mpvue 单文件页面配置详解
2018/12/02 Javascript
js中int和string数据类型互相转化实例
2019/01/16 Javascript
微信小程序自定义弹窗实现详解(可通用)
2019/07/04 Javascript
[03:20]次级联赛厮杀超职业 现超级兵对拆世纪大战
2014/10/30 DOTA
pycharm 使用心得(八)如何调用另一文件中的函数
2014/06/06 Python
python广度优先搜索得到两点间最短路径
2019/01/17 Python
keras在构建LSTM模型时对变长序列的处理操作
2020/06/29 Python
keras训练浅层卷积网络并保存和加载模型实例
2020/07/02 Python
如何避免常见的6种HTML5错误用法
2017/11/06 HTML / CSS
Expedia法国:全球最大在线旅游公司
2018/09/30 全球购物
解决方案设计综合面试题
2015/08/31 面试题
电子技术专业中专生的自我评价
2013/12/17 职场文书
教育专业自荐书范文
2013/12/17 职场文书
2014年公司庆元旦活动方案
2014/03/05 职场文书
国庆庆典邀请函
2015/02/02 职场文书
幼儿园中班个人总结
2015/02/28 职场文书
大学毕业生自我评价
2015/03/02 职场文书
学生会招新宣传语
2015/07/13 职场文书