壹 ❀ 引
如果有人问,在angularjs中修改模型数据为何视图会同步更新呢,我想大多数人一定会回答脏检查(Dirty Checking)相关概念。没错,在angularjs中作用域(scope)作为链接控制器(controller)与视图(view)之间的桥梁,除了绑定数据监听事件外,一旦有数据发生改变,scope还兼顾了脏检测更新视图的职责,这是我们宏观的理解。
这就引发了一系列的问题,以点击事件为例,为什么在angularjs中用原生click事件达不到更新视图的效果?ng-click与原生click有何区别?ng-click触发后angularjs又是怎么让视图更新的呢?$digest和$apply这两个眼熟的方法究竟有何作用,两者有什么区别?如果你对于这些问题感兴趣,不妨静下心来读一读本文,那么本文开始。
贰 ❀ angularjs的数据绑定
现在有个需求,当我们点击按钮时需要更新视图中的文本信息,当然不通过angularjs我们使用原生js做法也能轻易实现,像这样:
<div>我的名字是:<span class="name"></span></div>
<button class="btn">click me</button>
let btn = document.querySelector('.btn');
let name = document.querySelector('.name');
btn.onclick = function () {
name.innerHTML = '听风是风';
};
但这样做就有两个问题,第一我们不得不操作DOM,第二不便于复用,如果我们希望点击后将name字段更新到DOM不同层级的各种地方,此时获取DOM就尤为复杂了。
而angularjs便提供了一种有效的解决方法---数据绑定,它将我们需要更新的name字段抽离成了一份数据,在使用时你不用关心这份数据与DOM结构的内在联系,你要考虑的仅仅是在何处放置这份数据而已。
同样还是上面的需求,我们使用angularjs就可以这么做:
<body ng-controller="myCtrl as vm">
<div>我的名字是:<span>{{vm.name}}</span></div>
<button class="btn" ng-click="vm.sayName()">click me</button>
</body>
angular.module('myApp', [])
.controller('myCtrl', function () {
let vm = this;
vm.name = '';
vm.sayName = function () {
vm.name = '听风是风';
};
});
我们仔细对比这两种实现,js是click事件作为媒介找到对应的DOM并操作DOM,而angularjs通过click事件操作的却只是数据,前者操作DOM后者操作数据。突然想起了事件驱动与数据驱动的概念,也有那么点意思了。
这就比较神奇了,当我们点击按钮,name的值发生了改变,同时视图上也同步进行了刷新,angularjs是如何感知变化,又是怎么通过到视图的呢?这就得介绍$digest循环了。
叁 ❀ 神器的$digest
angularjs的事件循环又称为$digest循环,循环过程中包含了数据的脏检测,准确来说angularjs的脏检测功能是由scope上的$digest()方法实现,这里我们先理解$digest与脏检测的关系。
angularjs中的$digest循环主要包含了$watch列表与$evalAsync列表两个部分,$evalAsync列表先不分析,看到$watch列表大家是不是有点想法了呢?
没错,这里的$watch列表就是一个包含了多个$watch监听的数组,在scope中以$$watchers字段表示。$watch大家都不会陌生,监听一份数据,如果发生改变则执行对应回调,而angularjs便是利用$watch监听了我们需要交互的每份数据,只要发生改变,底层将通知视图进行更新。
说到这大家就纳闷了,我编程中明明没加$watch,哪来的呢?其实在angularjs使用中,无论是表达式{{}}还是ng-bind,凡是与视图上与数据交互的地方angualrjs都会帮你去注册watch监听,我们来看个简单的例子:
<body ng-app="myApp">
<div ng-controller="myCtrl as vm">
纯路人,{{vm.name}}非常{{vm.describe}}
<div ng-bind="vm.age"></div>
</div>
</body>
angular.module('myApp', [])
.controller('myCtrl', function () {
var vm = this;
vm.name = "听风是风";
vm.describe = '帅';
vm.age = 26;
});
在这个例子中,我们定义了三个属性,并在视图上与之绑定,查看当前控制器scope属性下的$$watchers属性(这个需要谷歌插件才能查看,插件名 ng-inspect for AngularJS),可以看到类型为Array,数组中包含的三个监听器分别对应我们前面定义的数据。一旦数据发生变化,$watch回调负责更新视图。
一个新的问题就是,angularjs的$watch又是如何感知哪些值发生了变化呢,这就像ng-click能执行代码是依赖了点击行为,毕竟总不能用定时器一直监听吧。
关于这一点就又回到了我们前面提到的$digest循环上了,angularjs在每次调用$scope.$digest()方法都会发起$digest循环,在循环中angularjs会触发$$watchers中的每一个$watch(脏检测),有了触发源$watch要做的就是新旧值对比,以及发生变化后的相应操作了。
OK,到这里我们明白了调用$socpe.$digest()会触发$digest循环,在循环中又会触发所有$watch进行数据对比,也就是我们说的脏检查,以及在数据变更后对视图进行更新。
那么$apply与$digest又有什么联系呢?我们接着说。
肆 ❀ $apply与$digest
在angularjs开发中,大家一定有过这样的经历,如果一段断码明明修改了数据但视图没变化,用$scope.$apply方法包裹代码就能解决该问题。比如我们在前文中click与ng-click的例子,这是为什么?
不卖关子,click之所以无法触发视图更新,这是因为click绑定函数中的函数作用域已经脱离了angularjs的上下文,angularjs的$digest循环无法感知脱离angularjs作用域的数据变化(你变了我不知道)。
使用$apply就能让angularjs执行脏检查的本质其实就是$apply也触发了$digest循环,准确来说,执行$scope.$apply后会调用$rootscope.$digest,所以只要使用了$apply方法,angularjs都 会从根作用域开始遍历每个作用域中的每个$warchers。
相比之下像angularjs中内置的事件比如ng-click都内置了$apply用于触发$digest循环,如果我们依旧使用$apply,angularjs反而会报错告诉你已经启动了$apply,一个简单的例子:
<button class="btn" ng-click="vm.sayName()">click me</button>
angular.module('myApp', [])
.controller('myCtrl', function ($scope) {
let vm = this;
vm.name = '';
vm.sayName = function () {
$scope.$apply(function () {
vm.name = '听风是风';
});
};
});
OK,打到这里我们知道了$apply方法执行会调用$rootscope.$digest方法,从而启动全新的$digest循环,对所有作用域中的数据进行脏检查。
伍 ❀ 何时使用$apply
在angularjs controller中的任何地方都属于angularjs的上下文,在这个上下文中直接修改变量都不需要$apply,但如果你在普通函数以及非angularjs提供的回调函数中修改变量,此时都需要结合$apply来通知angularjs进行额外的脏检查。举几个例子:
1.普通事件绑定的函数内修改数据需要使用$apply,文章开头已有举例。
2.普通定时器回调中修改数据需要使用$apply,一般推荐使用angularjs封装的定时器,比如$timeout:
//angularjs定时器
$timeout(() => vm.name = '听风是风', 1000);
//普通定时器
setTimeout(() => {
$scope.$apply(() => vm.name = '时间跳跃');
}, 2000);
在上文中我们已经说了,如果调用了$apply 等同于$rootscope.$digest,这样性能其实是不太好的,特别是存在多个scope的情况下,我们往往更喜欢只检测当前作用域的数据变化。
更加优化的做法是在当前作用域调用$digest,像这样:
setTimeout(() => {
$scope.$apply(() => vm.name = '时间跳跃');
}, 1000); setTimeout(function () {
vm.name = '听风是风';
$scope.$digest();
}, 2000);
陆 ❀ 总
那么到这里,我们详细介绍了$digest与$apply方法的区别,在介绍$digest循环后了解到$digest是由$apply触发,从而也解释了ng-click与普通click的区别。在介绍了$apply后,我们简单提及了使用$apply的场景,我们知道它会让angualrjs从根作用域开始脏检测,代价较大,因此推荐使用$digest可代替。那么到这里,本文结束。
参考
理解Angular中的$apply()以及$digest()
angularjs权威指南