进入正文前的说明:本文中的示例代码并不是AngularJs源码,而是来自书籍<<Build Your Own AngularJs>>, 这本书的做者仅依赖jquery和lodash一步一步构建出AngularJs的各核心模块,对全面理解AngularJs有很是巨大的帮助。如有正在使用AngulaJs攻城拔寨而且但愿彻底掌握手中武器的小伙伴,请前往 https://teropa.info/build-your-own-angular 进行购买阅读,相信能对你理解AngularJs带来莫大帮助,感谢做者。javascript
在这篇文章中,但愿能让您理清楚如下几项与scope相关的功能:java
如今开始咱们的第一部分:scope和dirty-checkingjquery
dirty-checking(脏检测)原理简述:scope经过$watch方法向this.$$watchers数组中添加watcher对象(包含watchFn, listenerFn, valueEq, last 四个属性)。每当$digest循环被触发时,它会遍历$$watchers数组,执行watcher中的watchFn,获取当前scope上某属性的值(一个watcher对应scope上一个被监听属性),而后去同watcher中的last(上一次的值)作比较,若两值不相等,就执行listenerFn。ajax
1 function Scope() { 2 this.$$watchers = []; // 监听器数组 3 this.$$lastDirtyWatch = null; // 每次digest循环的最后一个脏的watcher, 用于优化digest循环 4 this.$$asyncQueue = []; // scope上的异步队列 5 this.$$applyAsyncQueue = []; // scope上的异步apply队列 6 this.$$applyAsyncId = null; //异步apply信息 7 this.$$postDigestQueue = []; // postDigest执行队列 8 this.$$phase = null; // 储存scope上正在作什么,值有:digest/apply/null 9 this.$root = this; // rootScope 10 11 this.$$listeners = {}; // 存储包含自定义事件键值对的对象 12 13 this.$$children = []; // 存储当前scope的儿子Scope,以便$digest循环递归 14 }
实际上scope就是一个普通的javascript对象,一个类构造函数,能够经过new进行实例化。根据脏检测的原理,接下来,咱们一块儿看看scope的$watch方法的实现。express
1 /* $watch方法:向watchers数组中添加watcher对象,以便对应调用 */ 2 Scope.prototype.$watch = function(watchFn, listenerFn, valueEq) { 3 var self = this; 4 5 watchFn = $parse(watchFn); 6 7 // watchDelegate: 针对watch expression是常量和 one-time-binding的状况,进行优化。在第一次初始化以后删除watch 8 if(watchFn.$$watchDelegate) { 9 return watchFn.$$watchDelegate(self, listenerFn, valueEq, watchFn); 10 } 11 var watcher = { 12 watchFn: watchFn, 13 listenerFn: listenerFn || function() {}, 14 valueEq: !!valueEq, 15 last: initWatchVal 16 }; 17 18 this.$$watchers.unshift(watcher); 19 this.$root.$$lastDirtyWatch = null; 20 21 return function() { 22 var index = self.$$watchers.indexOf(watcher); 23 if(index >= 0) { 24 self.$$watchers.splice(index, 1); 25 self.$root.$$lastDirtyWatch = null; 26 } 27 }; 28 };
$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。app
从代码中可以看出,$watch的功能其实很是简单,就是构造watcher对象,并将watcher对象插入到scope.$$watchers数组中,而后返回一个销毁当前watcher的函数。框架
接下来进入到脏检测最核心的部分:$digest循环
《Build your own AngularJs》的做者将$digest分红了两个函数:$digestOnce 和 $digest。这虽然不用与框架源码,但可以使代码更易理解。两个函数实际上分别对应了$digest的内层循环和外层循环。代码以下:
内层循环
1 Scope.prototype.$$digestOnce = function() { 2 var dirty; 3 var continueLoop = true; 4 var self = this; 5 6 this.$$everyScope(function(scope) { 7 var newValue, oldValue; 8 9 _.forEachRight(scope.$$watchers, function(watcher) { 10 try { 11 if(watcher) { 12 newValue = watcher.watchFn(scope); 13 oldValue = watcher.last; 14 15 if(!scope.$$areEqual(newValue, oldValue, watcher.valueEq)) { 16 scope.$root.$$lastDirtyWatch = watcher; 17 18 watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); 19 20 watcher.listenerFn(newValue, 21 (oldValue === initWatchVal? newValue : oldValue), scope); 22 dirty = true; 23 } else if(scope.$root.$$lastDirtyWatch === watcher) { 24 continueLoop = false; 25 return false; 26 } 27 } 28 } catch(e) { 29 console.error(e); 30 } 31 }); 32 return continueLoop; 33 }); 34 35 return dirty; 36 };
代码中,$$everyScope是递归childScope执行回调函数的工具方法,后面会贴出。
$digestOnce的核心逻辑就在$$everyScope方法的循环体内,即遍历scope.$$watchers, 比对新旧值,根据比对结果肯定是否执行listenerFn,并向listenerFn中传入newValue, oldValue, scope供开发者获取。
示例代码第18行,watcher.last的赋值证明了上文提到的$watch的第三个参数valueEq的做用。
示例代码第23行,因为$digest循环会一直运行直到没有dirty watcher时,故单次$digest循环经过缓存最后一个dirty的watcher,在下一次$digest循环时若是遇到$$lastDirtyWatcher就中止当前循环。这样作减小了遍历watcher的数量,优化了性能。
外层循环
在咱们的示例中,外层循环即由 $digest来控制。$digest函数主要由do while循环体内调用$digestOnce进行脏检测 以及 对其余一些异步操做的处理组成。代码以下:
1 // digest循环的外循环,保持循环直到没有脏值为止 2 Scope.prototype.$digest = function() { 3 var ttl = TTL; 4 var dirty; 5 this.$root.$$lastDirtyWatch = null; 6 7 this.$beginPhase('$digest'); 8 9 if(this.$root.$$applyAsyncId) { 10 clearTimeout(this.$root.$$applyAsyncId); 11 this.$$flushApplyAsync(); 12 } 13 14 do { 15 while (this.$$asyncQueue.length) { 16 try { 17 var asyncTask = this.$$asyncQueue.shift(); 18 asyncTask.scope.$eval(asyncTask.expression); 19 } catch(e) { 20 console.error(e); 21 } 22 } 23 24 dirty = this.$$digestOnce(); 25 26 if((dirty || this.$$asyncQueue.length) && !(ttl--)) { 27 this.$clearPhase(); 28 throw TTL + ' digest iterations reached'; 29 } 30 } while (dirty || this.$$asyncQueue.length); 31 this.$clearPhase(); 32 33 while(this.$$postDigestQueue.length) { 34 try { 35 this.$$postDigestQueue.shift()(); 36 } catch(e) { 37 console.error(e); 38 } 39 } 40 };
在这一节中咱们的主要关注点是脏检测,异步任务相关的$$applyAsync,$$flushApplyAsync,$$asyncQueue,$$postDigestQueue以后再作分析。
示例代码第24行,调用$$digestOnce,并把返回值赋值给dirty。在do while循环中,只要dirty为true,那么循环就会一直执行下去,直到dirty的值为 false。这就是脏检测机制的外层循环的实现,是否是以为其实很简单呢,嘿嘿。
设想一下,某些值可能会在listenerFn中持续被改变而且,没法稳定下来,那势必会出现死循环。为了解决这个问题,AngularJs使用 TTL(time to live)来对循环次数进行控制,超过最大次数,就会throw错误 并 告诉开发者循环可能永远不会稳定。
如今咱们把注意力移到代码第26行的 if 代码块上,不难看出,这里是对最大$digest循环次数进行了限制,每执行一次do while循环的循环体,TTL就会自减1。当TTL值为0,再进行循环就会报错。固然咯,这个TTL的值也是可以进行配置的。
如今,相信小伙伴们对$digest循环已经比较清楚了吧~简单来讲,dirty-checking就是依赖缓存在scope上的$$watchers和$digest循环来对值进行监听的。有了$digest,固然还须要有手段去触发它咯。
接下来,咱们将进入第二部分:触发$digest循环 和 异步任务处理
$eval
说到触发$digest循环,大部分同窗都会想到$apply。要说$apply就须要先说说$eval。
$eval使咱们可以在scope的context中执行一段表达式,并容许传入locals object对当前scope context进行修改。
tip:$parse服务可以接受一个表达式或者函数做为参数,通过处理返回一个函数供开发者调用。这个函数有两个参数context object(一般就是scope),locals object(本地对象,经常使用来覆盖context中的属性)。
1 Scope.prototype.$eval = function(expr, locals) { 2 return $parse(expr)(this, locals); 3 };
$apply
$apply 方法接收一个expression或者function做为参数,$apply经过$eval函数执行传入的expression 或 function。最终从$rootScope上触发$digest循环。
$apply 被认为是 使AngularJs与第三方库混合使用最标准的方式。初学者朋友刚开始都会遇到用第三方库修改了scope上的属性或者被watch的属性,但并无触发$digest循环,致使双向绑定失效的问题。此时,$apply就是解决这种状况的良药!
1 Scope.prototype.$apply = function(expr) { 2 try { 3 this.$beginPhase('$apply'); 4 return this.$eval(expr); 5 } finally { 6 this.$clearPhase(); 7 this.$root.$digest(); 8 } 9 };
$apply本质上,就是用$eval执行了一段表达式,再调用rootScope的$digest方法。
有时候,当咱们可以肯定咱们不须要从rootScope开始进行$digest循环时,我能够调用scope.digest() 来代替 $apply,这样可以带来性能的提高。
$evalAsync
$evalAsync 用于延迟执行一段表达式。一般咱们更习惯使用$timeout服务来进行代码的延迟执行,但$timeout会将执行控制权交给浏览器,若是浏览器同时还须要执行诸如 ui渲染/事件控制/ajax 等任务时,咱们代码延迟执行的时机就会变得很是不可控。
咱们来看看$evalAsync是如何让代码延迟执行的时机变得严格,可控的。
1 Scope.prototype.$evalAsync = function(expr) { 2 var self = this; 3 if(!self.$$phase && !self.$$asyncQueue.length) { 4 setTimeout(function() { 5 if(self.$$asyncQueue.length) { 6 self.$root.$digest(); 7 } 8 }, 0); 9 } 10 11 this.$$asyncQueue.push({ 12 scope: this, 13 expression: expr 14 }); 15 };
$evalAsync方法的主要功能是从代码第11行开始,向$$asyncQueeu中添加对象。$$asyncQueue队列的执行是在$digest的do while循环中进行的。
while (this.$$asyncQueue.length) { try { var asyncTask = this.$$asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch(e) { console.error(e); } }
$evalAsync的代码会在正在运行的$digest循环中被执行,若是当前没有正在运行的$digest循环,会本身延迟触发一个$digest循环来执行延迟代码。
$applyAsync
$applyAsync用于合并短期内屡次$digest循环,优化应用性能。
在平常开发工做中,经常会遇到要短期内接收若干http响应,同时触发屡次$digest循环的状况。使用$applyAsync可合并若干次$digest,优化性能。
/* 这个方法用于 知道须要在短期内屡次使用$apply的状况, 可以对短期内屡次$digest循环进行合并, 是针对$digest循环的优化策略 */ Scope.prototype.$applyAsync = function(expr) { var self = this; self.$$applyAsyncQueue.push(function() { self.$eval(expr); }); if(self.$root.$$applyAsyncId === null) { self.$root.$$applyAsyncId = setTimeout(function() { self.$apply(_.bind(self.$$flushApplyAsync, self)); }, 0); } };
$$postDigest
$$postDigest方法提供了在下一次digest循环后执行代码的方式,这个方法的前缀是"$$",是一个AngularJs内部方法,应用开发极少用到。
此方法不自主触发$digest循环,而是在别处产生$digest循环以后执行。
1 /* $$postDigest 用于在下一次digest循环后执行函数队列 2 不一样于applyAsync 和 evalAsync, 它不触发digest循环 3 */ 4 Scope.prototype.$$postDigest = function(fn) { 5 this.$$postDigestQueue.push(fn); 6 };
到这里,咱们对脏检测的原理,即它的工做机制就了解的差很少了。但愿这些知识可以帮助你更好的应用AngularJs来开发,可以更轻松地定位错误。
下一章,我会继续为你们介绍文章开头提到的另外两处scope相关的特性。篇幅较长,感谢您的耐心阅读~