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 相关文章推荐
学习YUI.Ext 第四天--对话框Dialog的使用
Mar 10 Javascript
JavaScript中URL编码函数代码
Jan 11 Javascript
jQuery AJAX实现调用页面后台方法和web服务定义的方法分享
Mar 01 Javascript
JQuery 文本框回车跳到下一个文本框示例代码
Aug 30 Javascript
JavaScript框架是什么?怎样才能叫做框架?
Jul 01 Javascript
angular2使用简单介绍
Mar 01 Javascript
拥Bootstrap入怀——导航栏篇
May 30 Javascript
JS实现title标题栏文字不间断滚动显示效果
Sep 07 Javascript
BootStrap3使用错误记录及解决办法
Dec 22 Javascript
Vue.js实现移动端短信验证码功能
Mar 29 Javascript
JavaScript实现旋转木马轮播图
Mar 16 Javascript
Element Dialog对话框的使用示例
Jul 26 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
Ajax实时验证用户名/邮箱等是否已经存在的代码打包
2011/12/01 PHP
php遍历类中包含的所有元素的方法
2015/05/12 PHP
用ADODB.Stream转换
2007/01/22 Javascript
js function定义函数使用心得
2010/04/15 Javascript
简短几句 通俗解释javascript的闭包
2011/01/17 Javascript
优化innerHTML操作(提高代码执行效率)
2011/08/20 Javascript
javascript学习笔记(十二) RegExp类型介绍
2012/06/20 Javascript
利用JQuery制作符合Web标准的QQ弹出消息
2014/01/14 Javascript
js实现点击链接后窗口缩小并居中的方法
2015/03/02 Javascript
javascript编写贪吃蛇游戏
2015/07/07 Javascript
微信小程序 图片绝对定位(背景图片)
2017/04/05 Javascript
详解Vue 事件驱动和依赖追踪
2017/04/22 Javascript
Vuejs2 + Webpack框架里,模拟下载的实例讲解
2018/09/05 Javascript
JS+canvas画布实现炫酷的旋转星空效果示例
2019/02/13 Javascript
详解在Vue.js编写更好的v-for循环的6种技巧
2020/04/14 Javascript
[01:16:12]完美世界DOTA2联赛PWL S2 FTD vs Inki 第一场 11.21
2020/11/23 DOTA
Python splitlines使用技巧
2008/09/06 Python
python实现的简单FTP上传下载文件实例
2015/06/30 Python
Python实现优先级队列结构的方法详解
2016/06/02 Python
django 创建过滤器的实例详解
2017/08/14 Python
wxpython实现图书管理系统
2018/03/12 Python
Python 中 function(#) (X)格式 和 (#)在Python3.*中的注意事项
2018/11/30 Python
django框架实现模板中获取request 的各种信息示例
2019/07/01 Python
Python如何调用外部系统命令
2019/08/07 Python
Python Pillow.Image 图像保存和参数选择方式
2020/01/09 Python
TensorFlow实现打印每一层的输出
2020/01/21 Python
将pymysql获取到的数据类型是tuple转化为pandas方式
2020/05/15 Python
Python中的wordcloud库安装问题及解决方法
2020/05/27 Python
css3动画过渡实现鼠标跟随导航效果
2018/02/08 HTML / CSS
探矿工程师自荐信
2014/01/24 职场文书
领导班子在批评与自我批评座谈会上的发言
2014/09/28 职场文书
学生检讨书如何写
2014/10/30 职场文书
布达拉宫导游词
2015/02/02 职场文书
2015年生活老师工作总结
2015/05/27 职场文书
新学期主题班会
2015/08/17 职场文书
Nginx HTTP跳转至HTTPS
2022/05/15 Servers