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

引子

这篇文章是写给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 的值随之变化,而且能显示变化的次数。

$watch 检测到的数据变化

小结

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

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

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

(0)

相关推荐

  • angularJS中$apply()方法详解

    对于一个在前端属于纯新手的我来说,Javascript都还是一知半解,要想直接上手angular JS,遇到的阻力还真是不少.不过我相信,只要下功夫,即使是反人类的设计也不是什么大的问题. Okay,废话不多说.为了弄明白angular JS为何物,我先是从Scope开始.那么什么是Scope呢?借用官方文档的一段话: 复制代码 代码如下: "scope is an object that refers to the application model. It is an execution c

  • angularjs 中$apply,$digest,$watch详解

    如果你对angular的$apply,$digest,$watch似懂非懂,那我相信下面几句话能让你深刻理解! 此文针对已经了解过$apply,$digest,$watch的同学.也就是说你已经在很多的博客论坛搜索过$apply,$digest,$watch,虽然有点蒙,但似懂非懂的感觉. 如果你从未进行过了解,那本文对你将没有一点帮助! <!DOCTYPE html> <html lang="zh-CN" ng-app="app"> <

  • 浅谈angular.js中实现双向绑定的方法$watch $digest $apply

    Angular.js 中的特性,双向绑定. 多么神奇的功能,让视图的改变直接反应到数据中,数据的改变又实时的通知到视图,如何做到的? 这要归功于 scope 下面3个重要的方法: $watch $digest $apply 他们的区别是什么,我们来介绍下: $watch 这是一个监听 scope 上数据的监听器 方法说明: $scope.$watch('参数',function(newValue,oldValue){ //逻辑处理 }) 上面我们就是创建了一个监听器. '参数' 就是$scope

  • Angular.js中$apply()和$digest()的深入理解

    $apply()和$digest()介绍 AngularJS提供了一个非常酷的特性叫做双向数据绑定(Two-way Data Binding),这个特性大大简化了我们的代码编写方式.数据绑定意味着当View中有任何数据发生了变化,那么这个变化也会自动地反馈到scope的数据上,也即意味着scope模型会自动地更新.类似地,当scope模型发生变化时,view中的数据也会更新到最新的值.那么AngularJS是如何做到这一点的呢?当你写下表达式如 时,AngularJS在幕后会为你在scope模型

  • AngularJS中$apply方法和$watch方法用法总结

    本文实例总结了AngularJS中$apply方法和$watch方法用法.分享给大家供大家参考,具体如下: 引言 最近在项目中封装控件的时候用到了$watch方法来监听module中的值的变化,当时小编对这个方法不是很了解,所以在网上找了一些资料来学习一下,下面小编就给大家简单介绍一些AngularJS中Scope 提供$apply 方法传播Model 的变化和$watch方法监听module变化. $apply使用情景 AngularJS 外部的控制器(DOM 事件.外部的回调函数如 jQue

  • Angular项目中$scope.$apply()方法的使用详解

    前言 相信大家在一开始用angular做项目的时候,一定碰到过$scope.$apply()方法,表面上看,这像是一个帮助你进行数据更新的方法,那么,它为何存在,我们又该如何使用它呢.下面话不多说,来一起看看详细的介绍吧. JavaScript执行顺序 JavaScript单线程操作,代码按照代码片段的顺序来之行,每个代码块从运行到结束都不会被打断,这也是为什么会发生浏览器阻塞的情况,往往是有一部分在运行,而导致其他所有的代码段冻结. 每当有耗费时间较多的任务出现,例如等待一个click事件,等

  • AngularJS中的$watch(),$digest()和$apply()区分

    AngularJS $scope里面的$watch(),$digest()和$apply()是AngularJS的核心函数,学习AngularJS必须理解这几个函数. 在绑定$scope中的变量到view的时候,AngularJS自动在内部创建一个"Watch"."Watch"用于监听AngularJS scope中变量的改变.可以通过调用$scope.$watch()这个方法来创建"Watch". $scope.$digest()函数会循环访问

  • Angularjs中的$apply及优化使用详解

    前言 对于一个在前端属于纯新手的我来说,Javascript都还是一知半解,要想直接上手angular JS,遇到的阻力还真是不少.不过我相信,只要下功夫,即使是反人类的设计也不是什么大的问题. 今天,我们要聊得是Angularjs中的小明星$apply.当我们数据更新了,但是view层却没反应时,总能听到有人说,用apply吧,然后,懵懂无知的我们,在赋值代码后面加了$scope.$apply() ,然后就惊喜的发现.噢,真的更新了. 然而,有些时候,编译器会无情的给你返回 Error: $d

  • 全面解析Angular中$Apply()及$Digest()的区别

    $apply()和$digest()在AngularJS中是两个核心概念,但是有时候它们又让人困惑.而为了了解AngularJS的工作方式,首先需要了解$apply()和$digest()是如何工作的.这篇文章旨在解释$apply()和$digest()是什么,以及在日常的编码中如何应用它们. 1.探索$apply()和$digest() 1.1.认识双向数据绑定和$watch(); AngularJS提供了一个非常酷的特性叫做双向数据绑定(Two-way Data Binding),这个特性大

  • AngularJS报错$apply already in progress的解决方法分析

    本文实例分析了AngularJS报错$apply already in progress的解决方法.分享给大家供大家参考,具体如下: 如果我们使用了AngularJS中的$scope.$apply()或者$scope.$digest(),我们很可能会遇到类似下面的错误,虽然这个错误没有太大影响,但是在日志中看起来还是很不爽的,日志中记录的异常或者错误,就应该是需要关注和解决的问题,否则就没有必要出现在日志中了. Error: [$rootScope:inprog] $apply already

随机推荐