深入理解AngularJs-scope的脏检查(一)

进入正文前的说明:本文中的示例代码并非AngularJs源码,而是来自书籍<<Build Your Own AngularJs>>, 这本书的作者仅依赖jquery和lodash一步一步构建出AngularJs的各核心模块,对全面理解AngularJs有非常巨大的帮助。若有正在使用AngulaJs攻城拔寨并且希望完全掌握手中武器的小伙伴,相信能对你理解AngularJs带来莫大帮助,感谢作者。

在这篇文章中,希望能让您理清楚以下几项与scope相关的功能:

1.dirty-checking(脏检测)核心机制,主要包括:$watch 和 $digest;

2.几种不同的触发$digest循环的方式:$eval, $apply, $evalAsync, $applyAsync;

3.scope的继承机制以及isolated scope;

4.依赖于scope的事件循环:$on, $broadcast, $emit.

现在开始我们的第一部分:scope和dirty-checking

dirty-checking(脏检测)原理简述:scope通过$watch方法向this.$$watchers数组中添加watcher对象(包含watchFn, listenerFn, valueEq, last 四个属性)。每当$digest循环被触发时,它会遍历$$watchers数组,执行watcher中的watchFn,获取当前scope上某属性的值(一个watcher对应scope上一个被监听属性),然后去同watcher中的last(上一次的值)做比较,若两值不相等,就执行listenerFn。

function Scope() { this.$$watchers = []; // 监听器数组 this.$$lastDirtyWatch = null; // 每次digest循环的最后一个脏的watcher, 用于优化digest循环 this.$$asyncQueue = []; // scope上的异步队列 this.$$applyAsyncQueue = []; // scope上的异步apply队列 this.$$applyAsyncId = null; //异步apply信息 this.$$postDigestQueue = []; // postDigest执行队列 this.$$phase = null; // 储存scope上正在做什么,值有:digest/apply/null this.$root = this; // rootScope this.$$listeners = {}; // 存储包含自定义事件键值对的对象 this.$$children = []; // 存储当前scope的儿子Scope,以便$digest循环递归 }

实际上scope就是一个普通的javascript对象,一个类构造函数,可以通过new进行实例化。根据脏检测的原理,接下来,我们一起看看scope的$watch方法的实现。

/* $watch方法:向watchers数组中添加watcher对象,以便对应调用 */ Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { var self = this; watchFn = $parse(watchFn); // watchDelegate: 针对watch expression是常量和 one-time-binding的情况,进行优化。在第一次初始化之后删除watch if(watchFn.$$watchDelegate) { return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn); } var watcher = { watchFn: watchFn, listenerFn: listenerFn || function() {}, valueEq: !!valueEq, last: initWatchVal }; this.$$watchers.unshift(watcher); this.$root.$$lastDirtyWatch = null; return function() { var index = self.$$watchers.indexOf(watcher); if(index >= 0) { self.$$watchers.splice(index, 1); self.$root.$$lastDirtyWatch = null; } }; };

$watch方法的参数:

watchFn-监视表达式,在使用$watch时,通常是传入一个expression, 经过$parse服务处理后返回一个监视函数,提供动态访问scope上属性值的功能,可以看作 function() { return scope.someValue; }。

listenerFn-监听函数,当$digest循环dirty时(即scope上$$watchers数组中有watcher监测到属性值变化时),执行的回调函数。

valueEq-是否全等监视,布尔值,valueEq默认为false,此时$watch对监视对象进行“引用监视”,如果被监视的表达式是原始数据类型,$watch能够发现改变。如果被监视的表达式是引用类型,由于引用类型的赋值只是将被赋值变量指向当前引用,故$watch认为没有改变。若需要对引用类型进行监视,则需要将valueEq设置为true,这是$watch会对被监视对象进行“全等监视”,在每次比较前会用angular.copy()对被监视对象进行深拷贝,然后用angular.equal()进行比对。虽然“全等监视”能够监视到所有改变,但如果被监视对象很大,性能肯定会大打折扣。所以应该根据实际情况来使用valueEq。

从代码中能够看出,$watch的功能其实非常简单,就是构造watcher对象,并将watcher对象插入到scope.$$watchers数组中,然后返回一个销毁当前watcher的函数。

接下来进入到脏检测最核心的部分:$digest循环

《Build your own AngularJs》的作者将$digest分成了两个函数:$digestOnce 和 $digest。这虽然不用与框架源码,但能够使代码更易理解。两个函数实际上分别对应了$digest的内层循环和外层循环。代码如下:

内层循环

Scope.prototype.$$digestOnce = function() { var dirty; var continueLoop = true; var self = this; this.$$everyScope(function(scope) { var newValue, oldValue; _.forEachRight(scope.$$watchers, function(watcher) { try { if(watcher) { newValue = watcher.watchFn(scope); oldValue = watcher.last; if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) { scope.$root.$$lastDirtyWatch = watcher; watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); watcher.listenerFn(newValue, (oldValue === initWatchVal? newValue : oldValue), scope); dirty = true; } else if(scope.$root.$$lastDirtyWatch === watcher) { continueLoop = false; return false; } } } catch(e) { console.error(e); } }); return continueLoop; }); return dirty; };

代码中,$$everyScope是递归childScope执行回调函数的工具方法,后面会贴出。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wyxyzs.html