[AngularJS面面观] 12. scope中的watch机制---第三种策略$watchC
如果你刚刚入门angular,你或许还在惊叹于angular的双向绑定是多么的方便,你也许在庆幸未来的前端代码中再也不会出现那么多繁琐的DOM操作了。 但是,一旦你的应用程序随着业务的复杂而复杂,你就会发现你手头的那些angular的知识似乎开始不够用了。为什么绑定的数据没有生效?为什么应用的速度越来越慢?为什么会出现莫名其妙的infinite digest异常?所以你开始尝试进阶,尝试弄清楚在数据绑定这个现象后面到底发生了什么。 相信能顺着前面数十篇文章看到这里的同学们,一定对angular是真爱吧。我们分析了scope中的digest循环的具体逻辑和很多细节,也见识到了angular中建立在继承树型结构之上的事件机制。本文继续聊聊关于scope的最后一个话题,watch策略。 基于引用以及值的watch策略由scope中的
而 /* * 第三个参数objectEquality的含义:使用对象值比对,而不是引用比对 */
$watch: function(watchExp,listener,objectEquality,prettyPrintExpression)
由于在angular中被双向绑定多半都是number,string这类基本类型(Primitive),因此引用比对就是默认的选项。如果你需要比较对象(Object),比如数组,字典对象等,那么就需要将上述$watch方法的第三个参数置为true。 定义了watcher后,那么在digest循环中就需要执行真正的比对操作了,判断一个watcher是否”脏”了的核心判断条件如下: // 用于判定一个watcher是否dirty的条件
if ((value = get(current)) !== (last = watch.last) &&
// 上述的objectEquality即为这里的watch.eq
!(watch.eq
? equals(value,last)
: (typeof value === 'number' && typeof last === 'number'
&& isNaN(value) && isNaN(last)))) {
dirty = true;
// 调用watcher上的listener
// ......
}
这里利用了&&操作符的”短路”特性,首先执行 当需要进行值比对时,会调用angular定义的一个全局函数equals进行逐字段的递归比较过程,它能够确保被比较的两个对象真的是完全一致的,无论这个对象中嵌套了多少层对象。而第二个看似很复杂的只是为了应对JavaScript中一个很神奇的定义: 这里还有一段示例程序,可以直接复制到本地的server上运行看看效果: <html ng-app="testApp">
<head> <meta charset="UTF-8"> <title>Watch Mechanism</title> <script src="//cdn.bootcss.com/angular.js/1.5.7/angular.min.js"></script> </head> <body ng-controller="mainCtrl"> </body> <script> var mod = angular.module('testApp',[]); mod.controller('mainCtrl',function($scope,$interval) { $scope.obj = { 'id': 1 }; $scope.$watch(function(scope) { return scope.obj; },function(newValue,oldValue,scope) { console.log(newValue,oldValue); }); // },true); $interval(function() { console.log('digest triggered'); $scope.obj.id += 1; },1000); }); </script> </html>
当在 当初看到这里的时候,我有一个疑问。既然判断条件首先会判断新值和旧值的引用是否相同,再决定是否进行深度的值比对。那么上面的实例中每次改变的只是对象中的一个字段,对象本身并没有改变啊,也就意味着引用也没有被改变? 确实这个疑问大概困扰了一会。直到我继续阅读当数据”脏”了的处理逻辑时,才恍然大悟: watch.last = watch.eq ? copy(value,null) : value;
这里调用了定义在Angular.js中的另一个工具方法copy。现在还不着急介绍这些工具方法,简而言之这个方法会创建出一个和源对象一模一样的新对象并赋予旧值。注意这个”新”字,这也就意味着新值和旧值在每次执行值比对的时候,都是两个素昧平生的对象。因此就能够顺利地通过第一层引用判断。 以上就是我们已经见过好多次的两种watch策略。那么这第三种策略到底是何方”黑科技”,有什么存在意义和价值呢?这便是我们下面着重要分析和讨论的地方。 为什么需要第三种策略一言以蔽之,因为需求和性能。 目前的两种策略是非黑即白,非此即彼的策略。 基于引用比对的策略太过于简单,对付一些基本类型还好,万一被watch的是数组,字典对象这类就完全没有用武之地了。而基于值的比对策略则又太过于凶残,对象上的任何嵌套元素都不放过,而且伴随而来的是频繁的对象深度复制操作,需要消耗CPU资源以及占用大量的内存。以至于当scope中watchers的数量比较多时会显著地影响到整体性能。 由于在一个angular应用中,被Digest Cycle关注到的并非只是一些基本类型的值(尽管这类值占了很大一部分),有时候应用需要根据业务逻辑来关注某些数组对象或者是字典对象的变化。那么如何来找一种折中的解决方案就是这第三种策略的诞生原因。 那么这第三种策略需要支持到何种程度呢,从API文档中我们可以找到答案: Shallow watches the properties of an object and fires whenever any of the properties change (for arrays,this implies watching the array items; for object maps,this implies watching the properties). If a change is detected,the listener callback is fired.
// 翻译如下
针对对象属性的浅层监视(Shallow Watch),当属性发生变化时触发(对于数组,指的是监视数组的元素;对于字典对象,指的是监视其属性)对应listener的回调操作。
这里的重点是浅层监视(Shallow Watch)。也就是它的方案是只监视对象中的第一层元素/属性,如果这些元素/属性还有嵌套属性就不在考虑范围之内。这反映的也是一种折中的思想,如果监视的层次太深了,那么和基于值的比对策略就没多少区别了。 源码分析下面开始看看angular中是如何实现这种策略的,首先我们看看源码: $watchCollection: function(obj,listener) {
$watchCollectionInterceptor.$stateful = true;
var self = this;
// the current value,updated on each dirty-check run
var newValue;
// a shallow copy of the newValue from the last dirty-check run,
// updated to match newValue during dirty-check run
var oldValue;
// a shallow copy of the newValue from when the last change happened
var veryOldValue;
// only track veryOldValue if the listener is asking for it
var trackVeryOldValue = (listener.length > 1);
var changeDetected = 0;
var changeDetector = $parse(obj,$watchCollectionInterceptor);
var internalArray = [];
var internalObject = {};
var initRun = true;
var oldLength = 0;
function $watchCollectionInterceptor(_value) {
newValue = _value;
var newLength,key,bothNaN,newItem,oldItem;
// If the new value is undefined,then return undefined as the watch may be a one-time watch
if (isUndefined(newValue)) return;
if (!isObject(newValue)) { // if primitive
if (oldValue !== newValue) {
oldValue = newValue;
changeDetected++;
}
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
oldItem = oldValue[i];
newItem = newValue[i];
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[i] = newItem;
}
}
} else {
if (oldValue !== internalObject) {
// we are transitioning from something which was not an object into object.
oldValue = internalObject = {};
oldLength = 0;
changeDetected++;
}
// copy the items to oldValue and look for changes.
newLength = 0;
for (key in newValue) {
if (hasOwnProperty.call(newValue,key)) {
newLength++;
newItem = newValue[key];
oldItem = oldValue[key];
if (key in oldValue) {
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[key] = newItem;
}
} else {
oldLength++;
oldValue[key] = newItem;
changeDetected++;
}
}
}
if (oldLength > newLength) {
// we used to have more keys,need to find them and destroy them.
changeDetected++;
for (key in oldValue) {
if (!hasOwnProperty.call(newValue,key)) {
oldLength--;
delete oldValue[key];
}
}
}
}
return changeDetected;
}
function $watchCollectionAction() {
if (initRun) {
initRun = false;
listener(newValue,newValue,self);
} else {
listener(newValue,veryOldValue,self);
}
// make a copy for the next time a collection is changed
if (trackVeryOldValue) {
if (!isObject(newValue)) {
//primitive
veryOldValue = newValue;
} else if (isArrayLike(newValue)) {
veryOldValue = new Array(newValue.length);
for (var i = 0; i < newValue.length; i++) {
veryOldValue[i] = newValue[i];
}
} else { // if object
veryOldValue = {};
for (var key in newValue) {
if (hasOwnProperty.call(newValue,key)) {
veryOldValue[key] = newValue[key];
}
}
}
}
}
return this.$watch(changeDetector,$watchCollectionAction);
}
前前后后一共130来行代码,将其中的主干逻辑提取一下后: 逻辑主干$watchCollection: function(obj,listener) {
// 各类变量的声明 - 供watch函数以及listener函数的代理共享
var changeDetector = $parse(obj,$watchCollectionInterceptor);
// watch函数的代理
function $watchCollectionInterceptor(_value) {
}
// listener函数的代理
function $watchCollectionAction() {
}
return this.$watch(changeDetector,$watchCollectionAction);
}
抛开实现的细节,整体逻辑还是比较清晰的。首先注意看 变量声明第一步,看看方法的头部都声明了什么变量: $watchCollectionInterceptor.$stateful = true;
var self = this;
// 当前被关注的值,在每轮DC中会被更新
var newValue;
// 上一轮DC中newValue的浅拷贝(Shallow Copy),在每轮DC中会被更新与newValue一致
var oldValue;
// 记录newValue的前值,是它的一份浅拷贝(Shallow Copy)
var veryOldValue;
// 是否记录veryOldValue的标志位
var trackVeryOldValue = (listener.length > 1);
// 判断watch是否触发listener的递增计数器
var changeDetected = 0;
// watch的代理
var changeDetector = $parse(obj,$watchCollectionInterceptor);
// 比对对象为数组时,oldValue的初值
var internalArray = [];
// 比对对象为字典对象时,oldValue的初值
var internalObject = {};
// listener是否首次触发的标志位
var initRun = true;
// 旧值数组对象长度的保存以及减少遍历字典对象次数的优化字段
var oldLength = 0;
上面列出了所有被定义在方法头部的变量,很多变量的用途和意义在没有看具体的实现之前是很难明白的,这里我事先给出了一个简要的解释,因此这里看不懂没有关系,待后面碰到了后再具体解释。 关于第一行中 watch代理的实现第二步,看看watch的代理是如何实现的: function $watchCollectionInterceptor(_value) {
newValue = _value;
var newLength,oldItem;
// 针对一次性watch的处理,一次性watch和一次性绑定相关,而一次性绑定就是形如<p>Hello {{::name}}!</p>的绑定方式,在绑定后就不会参与到digest cycle中
if (isUndefined(newValue)) return;
if (!isObject(newValue)) {
// 如果处理的newValue是基本类型(Primitives),仍然采用基于引用的watch策略
if (oldValue !== newValue) {
oldValue = newValue;
changeDetected++;
}
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// 将oldValue设置成一个数组对象
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// 首先比较长度,如果长度不一样则两个数组必定不同
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
oldItem = oldValue[i];
newItem = newValue[i];
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[i] = newItem;
}
}
} else {
if (oldValue !== internalObject) {
// 将oldValue设置成一个字典对象
oldValue = internalObject = {};
oldLength = 0;
changeDetected++;
}
// copy the items to oldValue and look for changes.
newLength = 0;
for (key in newValue) {
if (hasOwnProperty.call(newValue,key)) {
newLength++;
newItem = newValue[key];
oldItem = oldValue[key];
if (key in oldValue) {
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[key] = newItem;
}
} else {
oldLength++;
oldValue[key] = newItem;
changeDetected++;
}
}
}
if (oldLength > newLength) {
// 旧值上的字段个数多余当前值上的字段个数,需要删除多余的
changeDetected++;
for (key in oldValue) {
if (!hasOwnProperty.call(newValue,key)) {
oldLength--;
delete oldValue[key];
}
}
}
}
return changeDetected;
}
整个watch函数代理的逻辑还是比较清晰的,分为以下几个逻辑块: 重点就是如何处理”类数组类型(Array-like)”以及字典对象。 处理类数组类型(Array-like)下面是处理类数组类型的逻辑: else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// 将oldValue设置成一个数组对象
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// 首先比较长度,如果长度不一样则两个数组必定不同
changeDetected++;
oldValue.length = oldLength = newLength;
}
// 进行比对并将新的元素拷贝到oldValue上
for (var i = 0; i < newLength; i++) {
oldItem = oldValue[i];
newItem = newValue[i];
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[i] = newItem;
}
}
}
首先会调用 然后开始比较当前数组的长度和上一次数组的长度,如果长度不一致,则自增 处理字典对象类型(Object)下面是处理字典对象的逻辑: } else {
if (oldValue !== internalObject) {
// 将oldValue设置成一个字典对象
oldValue = internalObject = {};
oldLength = 0;
changeDetected++;
}
// 进行比对并将新的属性拷贝到oldValue上
newLength = 0;
for (key in newValue) {
if (hasOwnProperty.call(newValue,key)) {
newLength++;
newItem = newValue[key];
oldItem = oldValue[key];
if (key in oldValue) {
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
oldValue[key] = newItem;
}
} else {
oldLength++;
oldValue[key] = newItem;
changeDetected++;
}
}
}
if (oldLength > newLength) {
// 旧值上的字段个数多余当前值上的字段个数,需要删除多余的
changeDetected++;
for (key in oldValue) {
if (!hasOwnProperty.call(newValue,key)) {
oldLength--;
delete oldValue[key];
}
}
}
}
如果是第一次运行watch,那么此时 然后开始执行一个循环。这个循环的作用是将 listener代理的实现以上就是 function $watchCollectionAction() {
if (initRun) {
initRun = false;
listener(newValue,self);
} else {
listener(newValue,self);
}
// 如果需要追踪oldValue的值,创建一个拷贝
if (trackVeryOldValue) {
if (!isObject(newValue)) {
// 基本类型
veryOldValue = newValue;
} else if (isArrayLike(newValue)) {
veryOldValue = new Array(newValue.length);
for (var i = 0; i < newValue.length; i++) {
veryOldValue[i] = newValue[i];
}
} else {
veryOldValue = {};
for (var key in newValue) {
if (hasOwnProperty.call(newValue,key)) {
veryOldValue[key] = newValue[key];
}
}
}
}
}
angular对于listener有一个约定:第一次运行时 然后当 通过这一点可以知道在需要对一个数组或者字典对象使用 至此,watch的第三种策略 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |