AngularJS入门教程之数据绑定原理详解


Posted in Javascript onNovember 02, 2016

本文实例讲述了AngularJS数据绑定原理。分享给大家供大家参考,具体如下:

这篇文章主要是写给新手的,是给那些刚刚开始接触Angular,并且想了解数据帮定是如何工作的人。如果你已经对Angular比较了解了,那强烈建议你直接去阅读源代码。

Angular用户都想知道数据绑定是怎么实现的。你可能会看到各种各样的词汇:$watch,$apply,$digest,dirty-checking...它们是什么?它们是如何工作的呢?这里我想回答这些问题,其实它们在官方的文档里都已经回答了,但是我还是想把它们结合在一起来讲,但是我只是用一种简单的方法来讲解,如果要想了解技术细节,查看源代码。

让我们从头开始吧。

浏览器事件循环和Angular.js扩展

我们的浏览器一直在等待事件,比如用户交互。假如你点击一个按钮或者在输入框里输入东西,事件的回调函数就会在JavaScript解释器里执行,然后你就可以做任何DOM操作,等回调函数执行完毕时,浏览器就会相应地对DOM做出变化。 Angular拓展了这个事件循环,生成一个有时成为angular context的执行环境(记住,这是个重要的概念),为了解释什么是context以及它如何工作,我们还需要解释更多的概念。

$watch 队列($watch list)

每次你绑定一些东西到你的UI上时你就会往$watch队列里插入一条$watch。想象一下$watch就是那个可以检测它监视的model里时候有变化的东西。例如你有如下的代码

index.html

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";
});

index.html

Hello, {{ World }}

这里,即便我们在$scope上添加了两个东西,但是只有一个绑定在了UI上,因此在这里只生成了一个$watch.

再看下面的例子:

controllers.js

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

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。 因此,每一个绑定到了UI上的数据都会生成一个$watch。对,那这写$watch是什么时候生成的呢? 当我们的模版加载完毕时,也就是在linking阶段(Angular分为compile阶段和linking阶段---译者注),Angular解释器会寻找每个directive,然后生成每个需要的$watch。听起来不错哈,但是,然后呢?

$digest循环

还记得我前面提到的扩展的事件循环吗?当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发。这个循环是由两个更小的循环组合起来的。一个处理evalAsync队列,另一个处理$watch队列,这个也是本篇博文的主题。 这个是处理什么的呢?$digest将会遍历我们的$watch,然后询问:

嘿,$watch,你的值是什么?
是9。
好的,它改变过吗?
没有,先生。
(这个变量没变过,那下一个)
你呢,你的值是多少?
报告,是Foo。
刚才改变过没?
改变过,刚才是Bar。
(很好,我们有DOM需要更新了)
继续询问知道$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";
 }
});

index.html

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

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

我们按下按钮

浏览器接收到一个事件,进入angular context(后面会解释为什么)。

$digest循环开始执行,查询每个$watch是否变化。

由于监视$scope.name的$watch报告了变化,它会强制再执行一次$digest循环。

新的$digest循环没有检测到变化。

浏览器拿回控制权,更新与$scope.name新值相应部分的DOM。

这里很重要的(也是许多人的很蛋疼的地方)是每一个进入angular context的事件都会执行一个$digest循环,也就是说每次我们输入一个字母循环都会检查整个页面的所有$watch。

通过$apply来进入angular context

谁决定什么事件进入angular context,而哪些又不进入呢?$apply!

如果当事件触发时,你调用$apply,它会进入angular context,如果没有调用就不会进入。现在你可能会问:刚才的例子里我也没有调用$apply啊,为什么?Angular为了做了!因此你点击带有ng-click的元素时,时间就会被封装到一个$apply调用。如果你有一个ng-model="foo"的输入框,然后你敲一个f,事件就会这样调用$apply("foo = 'f';")。

Angular什么时候不会自动为我们$apply呢?

这是Angular新手共同的痛处。为什么我的jQuery不会更新我绑定的东西呢?因为jQuery没有调用$apply,事件没有进入angular context,$digest循环永远没有执行。

我们来看一个有趣的例子:

假设我们有下面这个directive和controller

app.js

app.directive('clickable', function() {
return {
 restrict: "E",
 scope: {
  foo: '=',
  bar: '='
 },
 template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',
 link: function(scope, element, attrs) {
  element.bind('click', function() {
   scope.foo++;
   scope.bar++;
  });
 }
}
});
app.controller('MainCtrl', function($scope) {
 $scope.foo = 0;
 $scope.bar = 0;
});

它将foo和bar从controller里绑定到一个list里面,每次点击这个元素的时候,foo和bar都会自增1。

那我们点击元素的时候会发生什么呢?我们能看到更新吗?答案是否定的。因为点击事件是一个没有封装到$apply里面的常见的事件,这意味着我们会失去我们的计数吗?不会

真正的结果是:$scope确实改变了,但是没有强制$digest循环,监视foo 和bar的$watch没有执行。也就是说如果我们自己执行一次$apply那么这些$watch就会看见这些变化,然后根据需要更新DOM。

试试看吧:http://jsbin.com/opimat/2/

如果我们点击这个directive(蓝色区域),我们看不到任何变化,但是我们点击按钮时,点击数就更新了。如刚才说的,在这个directive上点击时我们不会触发$digest循环,但是当按钮被点击时,ng-click会调用$apply,然后就会执行$digest循环,于是所有的$watch都会被检查,当然就包括我们的foo和bar的$watch了。

现在你在想那并不是你想要的,你想要的是点击蓝色区域的时候就更新点击数。很简单,执行一下$apply就可以了:

element.bind('click', function() {
 scope.foo++;
 scope.bar++;
 scope.$apply();
});

$apply是我们的$scope(或者是direcvie里的link函数中的scope)的一个函数,调用它会强制一次$digest循环(除非当前正在执行循环,这种情况下会抛出一个异常,这是我们不需要在那里执行$apply的标志)。

试试看:http://jsbin.com/opimat/3/edit

有用啦!但是有一种更好的使用$apply的方法:

element.bind('click', function() {
 scope.$apply(function() {
   scope.foo++;
   scope.bar++;
 });
})

有什么不一样的?差别就是在第一个版本中,我们是在angular context的外面更新的数据,如果有发生错误,Angular永远不知道。很明显在这个像个小玩具的例子里面不会出什么大错,但是想象一下我们如果有个alert框显示错误给用户,然后我们有个第三方的库进行一个网络调用然后失败了,如果我们不把它封装进$apply里面,Angular永远不会知道失败了,alert框就永远不会弹出来了。

因此,如果你想使用一个jQuery插件,并且要执行$digest循环来更新你的DOM的话,要确保你调用了$apply。

有时候我想多说一句的是有些人在不得不调用$apply时会“感觉不妙”,因为他们会觉得他们做错了什么。其实不是这样的,Angular不是什么魔术师,他也不知道第三方库想要更新绑定的数据。

使用$watch来监视你自己的东西

你已经知道了我们设置的任何绑定都有一个它自己的$watch,当需要时更新DOM,但是我们如果要自定义自己的watches呢?简单

来看个例子:

app.js

app.controller('MainCtrl', function($scope) {
 $scope.name = "Angular";
 $scope.updated = -1;
 $scope.$watch('name', function() {
  $scope.updated++;
 });
});

index.html

<body ng-controller="MainCtrl">
 <input ng-model="name" />
 Name updated: {{updated}} times.
</body>

这就是我们创造一个新的$watch的方法。第一个参数是一个字符串或者函数,在这里是只是一个字符串,就是我们要监视的变量的名字,在这里,$scope.name(注意我们只需要用name)。第二个参数是当$watch说我监视的表达式发生变化后要执行的。我们要知道的第一件事就是当controller执行到这个$watch时,它会立即执行一次,因此我们设置updated为-1。

试试看:http://jsbin.com/ucaxan/1/edit

例子2:

app.js

app.controller('MainCtrl', function($scope) {
 $scope.name = "Angular";
 $scope.updated = 0;
 $scope.$watch('name', function(newValue, oldValue) {
  if (newValue === oldValue) { return; } // AKA first run
  $scope.updated++;
 });
});

index.html

<body ng-controller="MainCtrl">
 <input ng-model="name" />
 Name updated: {{updated}} times.
</body>

watch的第二个参数接受两个参数,新值和旧值。我们可以用他们来略过第一次的执行。通常你不需要略过第一次执行,但在这个例子里面你是需要的。灵活点嘛少年。

例子3:

app.js

app.controller('MainCtrl', function($scope) {
 $scope.user = { name: "Fox" };
 $scope.updated = 0;
 $scope.$watch('user', function(newValue, oldValue) {
  if (newValue === oldValue) { return; }
  $scope.updated++;
 });
});

index.html

<body ng-controller="MainCtrl">
 <input ng-model="user.name" />
 Name updated: {{updated}} times.
</body>

我们想要监视$scope.user对象里的任何变化,和以前一样这里只是用一个对象来代替前面的字符串。

试试看:http://jsbin.com/ucaxan/3/edit

呃?没用,为啥?因为$watch默认是比较两个对象所引用的是否相同,在例子1和2里面,每次更改$scope.name都会创建一个新的基本变量,因此$watch会执行,因为对这个变量的引用已经改变了。在上面的例子里,我们在监视$scope.user,当我们改变$scope.user.name时,对$scope.user的引用是不会改变的,我们只是每次创建了一个新的$scope.user.name,但是$scope.user永远是一样的。

例子4:

app.js

app.controller('MainCtrl', function($scope) {
 $scope.user = { name: "Fox" };
 $scope.updated = 0;
 $scope.$watch('user', function(newValue, oldValue) {
  if (newValue === oldValue) { return; }
  $scope.updated++;
 }, true);
});

index.html

<body ng-controller="MainCtrl">
 <input ng-model="user.name" />
 Name updated: {{updated}} times.
</body>

试试看:http://jsbin.com/ucaxan/4/edit

现在有用了吧!因为我们对$watch加入了第三个参数,它是一个bool类型的参数,表示的是我们比较的是对象的值而不是引用。由于当我们更新$scope.user.name时$scope.user也会改变,所以能够正确触发。

关于$watch还有很多tips&tricks,但是这些都是基础。

总结

好吧,我希望你们已经学会了在Angular中数据绑定是如何工作的。我猜想你的第一印象是dirty-checking很慢,好吧,其实是不对的。它像闪电般快。但是,是的,如果你在一个模版里有2000-3000个watch,它会开始变慢。但是我觉得如果你达到这个数量级,就可以找个用户体验专家咨询一下了

无论如何,随着ECMAScript6的到来,在Angular未来的版本里我们将会有Object.observe那样会极大改善$digest循环的速度。同时未来的文章也会涉及一些tips&tricks。

另一方面,这个主题并不容易,如果你发现我落下了什么重要的东西或者有什么东西完全错了,请指正(原文是在GITHUB上PR 或报告issue)

希望本文所述对大家AngularJS程序设计有所帮助。

Javascript 相关文章推荐
高性能WEB开发 flush让页面分块,逐步呈现 flush让页面分块,逐步呈现
Jun 19 Javascript
也说JavaScript中String类的replace函数
Sep 22 Javascript
用JQUERY增删元素的代码
Feb 14 Javascript
js Dialog 去掉右上角的X关闭功能
Apr 23 Javascript
原生JS绑定滑轮滚动事件兼容常见浏览器
Jun 30 Javascript
js 求时间差的实现代码
Apr 26 Javascript
AJAX实现瀑布流触发分页与分页触发瀑布流的方法
May 23 Javascript
JS实现按钮添加背景音乐示例代码
Oct 17 Javascript
Vue-cli3项目配置Vue.config.js实战记录
Jul 29 Javascript
Vue中插入HTML代码的方法
Sep 21 Javascript
浅入深出Vue之组件使用
Jul 11 Javascript
JavaScript 替换所有匹配内容及正则替换方法
Feb 12 Javascript
深入理解Node.js 事件循环和回调函数
Nov 02 #Javascript
JavaScript 数组的深度复制解析
Nov 02 #Javascript
AngularJS实现与Java Web服务器交互操作示例【附demo源码下载】
Nov 02 #Javascript
Centos7 中 Node.js安装简单方法
Nov 02 #Javascript
AngularJS入门教程之与服务器(Ajax)交互操作示例【附完整demo源码下载】
Nov 02 #Javascript
用AngularJS来实现监察表单按钮的禁用效果
Nov 02 #Javascript
AngularJS入门教程之Cookies读写操作示例
Nov 02 #Javascript
You might like
php下检测字符串是否是utf8编码的代码
2008/06/28 PHP
php完全过滤HTML,JS,CSS等标签
2009/01/16 PHP
调整优化您的LAMP应用程序的5种简单方法
2011/06/26 PHP
PHP Curl出现403错误的解决办法
2014/05/29 PHP
ThinkPHP的MVC开发机制实例解析
2014/08/23 PHP
Yii2表单事件之Ajax提交实现方法
2017/05/04 PHP
php curl简单采集图片生成base64编码(并附curl函数参数说明)
2019/02/15 PHP
Alliance vs Liquid BO3 第一场2.13
2021/03/10 DOTA
jquery中的$(document).ready()与window.onload的区别
2009/11/18 Javascript
jQuery实现折线图的方法
2015/02/28 Javascript
详解JavaScript中循环控制语句的用法
2015/06/03 Javascript
JS图片定时翻滚效果实现方法
2016/06/21 Javascript
canvas实现动态小球重叠效果
2017/02/06 Javascript
VUE2 前端实现 静态二级省市联动选择select的示例
2018/02/09 Javascript
一种angular的方法级的缓存注解(装饰器)
2018/03/13 Javascript
如何编写一个d.ts文件的步骤详解
2018/04/13 Javascript
react 父子组件之间通讯props
2018/09/08 Javascript
jquery UI实现autocomplete在获取焦点时得到显示列表功能示例
2019/06/04 jQuery
Nuxt.js实现一个SSR的前端博客的示例代码
2019/09/06 Javascript
python&amp;MongoDB爬取图书馆借阅记录
2016/02/05 Python
python使用logging模块发送邮件代码示例
2018/01/18 Python
Django错误:TypeError at / 'bool' object is not callable解决
2019/08/16 Python
解决pycharm启动后总是不停的updating indices...indexing的问题
2019/11/27 Python
使用 prometheus python 库编写自定义指标的方法(完整代码)
2020/06/29 Python
python+selenium 简易地疫情信息自动打卡签到功能的实现代码
2020/08/22 Python
linux centos 7.x 安装 python3.x 替换 python2.x的过程解析
2020/12/14 Python
李宁官方网店:中国运动品牌
2017/11/02 全球购物
UGG美国官网:购买UGG雪地靴、拖鞋和鞋子
2017/12/31 全球购物
GANT葡萄牙官方商店:拥有美国运动服传统的生活方式品牌
2018/10/18 全球购物
美国牛仔品牌:True Religion
2018/11/16 全球购物
英国和世界各地预订便宜的酒店:LateRooms.com
2019/05/05 全球购物
企业门卫岗位职责
2013/12/12 职场文书
2015年网络管理员工作总结
2015/05/21 职场文书
2015年政治教研组工作总结
2015/07/22 职场文书
浅谈GO中的Channel以及死锁的造成
2022/03/18 Golang
Linux服务器离线安装 nginx的详细步骤
2022/06/16 Servers