上一篇地址html
上次说到了rootScope里的$watch方法中的解析监控表达式,即而引出了对parse的分析,今天咱们接着这里继续挖代码.git
先上一块$watch代码github
$watch: function(watchExp, listener, objectEquality) { var scope = this, get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, get: get, exp: watchExp, eq: !!objectEquality }; lastDirtyWatch = null; // in the case user pass string, we need to compile it, do we really need this ? if (!isFunction(listener)) { var listenFn = compileToFn(listener || noop, 'listener'); watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } if (typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; } if (!array) { array = scope.$$watchers = []; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; }; }
这里的get = compileToFn(watchExp, 'watch'),上篇已经分析完了,这里返回的是一个执行表达式的函数,接着往下看,这里初始化了一个watcher对象,用来保存一些监听相关的信息,简单的说明一下express
而后会检查传递进来的监听参数是否为函数,若是是一个有效的字符串,则经过parse来解析生成一个函数,不然赋值为一个noop占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前做用域.api
接着会检查监控表达式是否为字符串而且执行表达式的constant为true,表明这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数以后就会删除这个$watch.最后往当前做用域里的$$watchers数组头中添加$watch信息,注意这里的返回值,利用JS的闭包保留了当前的watcher,而后返回一个函数,这个就是用来删除监听用的.数组
这个$eval也是挺方便的函数,假如你想直接在程序里执行一个字符串的话,那么能够这么用闭包
$scope.name = '2'; $scope.$eval('1+name'); // ==> 会输出12
你们来看看它的函数体app
return $parse(expr)(this, locals);
其实就是经过parse来解析成一个执行表达式函数,而后传递当前做用域以及额外的参数,返回这个执行表达式函数的值异步
evalAsync函数的做用就是延迟执行表达式,而且执行完不论是否异常,触发dirty check.async
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { $browser.defer(function() { if ($rootScope.$$asyncQueue.length) { $rootScope.$digest(); } }); } this.$$asyncQueue.push({scope: this, expression: expr});
能够看到当前做用域内部有一个$$asyncQueue异步队列,保存着全部须要延迟执行的表达式,此处的表达式能够是字符串或者函数,由于这个表达式最终会调用$eval方法,注意这里调用了$browser服务的defer方法,从ng->browser.js源码里能够看到,其实这里就是调用setTimeout来实现的.
self.defer = function(fn, delay) { var timeoutId; outstandingRequestCount++; timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; completeOutstandingRequest(fn); }, delay || 0); pendingDeferIds[timeoutId] = true; return timeoutId; };
上面的代码主要是延迟执行函数,另外pendingDeferIds对象保存全部setTimeout返回的id,这个会在self.defer.cancel这里能够取消执行延迟执行.
说digest方法以前,还有一个方法要说说
这个方法跟evalAsync不一样的时,它不会主动触发digest方法,只是往postDigestQueue队列中增长执行表达式,它会在digest体内最后执行,至关于在触发dirty check以后,能够执行别的一些逻辑.
this.$$postDigestQueue.push(fn);
下面咱们来重点说说digest方法
digest方法是dirty check的核心,主要思路是先执行$$asyncQueue队列中的表达式,而后开启一个loop来的执行全部的watch里的监听函数,前提是先后两次的值是否不相等,假如ttl超过系统默认值,则dirth check结束,最后执行$$postDigestQueue队列里的表达式.
$digest: function() { var watch, value, last, watchers, asyncQueue = this.$$asyncQueue, postDigestQueue = this.$$postDigestQueue, length, dirty, ttl = TTL, next, current, target = this, watchLog = [], logIdx, logMsg, asyncTask; beginPhase('$digest'); lastDirtyWatch = null; do { // "while dirty" loop dirty = false; current = target; while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null; } traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value == 'number' && typeof last == 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // break traverseScopesLoop; takes us to here if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, toJson(watchLog)); } } while (dirty || asyncQueue.length); clearPhase(); while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } }
经过上面的代码,能够看出,核心就是两个loop,外loop保证全部的model都能检测到,内loop则是真实的检测每一个watch,watch.get就是计算监控表达式的值,这个用来跟旧值进行对比,假如不相等,则执行监听函数
注意这里的watch.eq这是是否深度检查的标识,equals方法是angular.js里的公共方法,用来深度对比两个对象,这里的不相等有一个例外,那就是NaN ===NaN,由于这个永远都是false,因此这里加了检查
!(watch.eq ? equals(value, last) : (typeof value == 'number' && typeof last == 'number' && isNaN(value) && isNaN(last)))
比较完以后,把新值传给watch.last,而后执行watch.fn也就是监听函数,传递三个参数,分别是:最新计算的值,上次计算的值(假如是第一次的话,则传递新值),最后一个参数是当前做用域实例,这里有一个设置外loop的条件值,那就是dirty = true,也就是说只要内loop执行了一次watch,则外loop还要接着执行,这是为了保证全部的model都能监测一次,虽然这个有点浪费性能,不过超过ttl设置的值后,dirty check会强制关闭,并抛出异常
if((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, toJson(watchLog)); }
这里的watchLog日志对象是在内loop里,当ttl低于5的时候开始记录的
if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); watchLog[logIdx].push(logMsg); }
当检查完一个做用域内的全部watch以后,则开始深度遍历当前做用域的子级或者父级,虽然这有些影响性能,就像这里的注释写的那样yes, this code is a bit crazy
// Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } }
上面的代码其实就是不断的查找当前做用域的子级,没有子级,则开始查找兄弟节点,最后查找它的父级节点,是一个深度遍历查找.只要next有值,则内loop则一直执行
while ((current = next))
不过内loop也有跳出的状况,那就是当前watch跟最后一次检查的watch相等时就退出内loop.
else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; }
注意这个内loop同时也是一个label(标签)语句,这个能够在loop中执行跳出操做就像上面的break
正常执行完两个loop以后,清除当前的阶段标识clearPhase();,而后开始执行postDigestQueue队列里的表达式.
while(postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } }
接下来讲说,用的也比较多的$apply方法
这个方法通常用在,不在ng的上下文中执行js代码的状况,好比原生的DOM事件中执行想改变ng中某些model的值,这个时候就要使用$apply方法了
$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }
代码中,首先让当前阶段标识为$apply,这个能够防止使用$apply方法时检查是否已经在这个阶段了,而后就是执行$eval方法, 这个方法上面有讲到,最后执行$digest方法,来使ng中的M或者VM改变.
接下来讲说scope中event模块,它的api跟通常的event事件模块比较像,提供有$on,$emit,$broadcast,这三个很实用的方法
这个方法是用来定义事件的,这里用到了两个实例变量$$listeners, $$listenerCount,分别用来保存事件,以及事件数量计数
$on: function(name, listener) { var namedListeners = this.$$listeners[name]; if (!namedListeners) { this.$$listeners[name] = namedListeners = []; } namedListeners.push(listener); var current = this; do { if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++; } while ((current = current.$parent)); var self = this; return function() { namedListeners[indexOf(namedListeners, listener)] = null; decrementListenerCount(self, 1, name); }; }
分析上面的代码,能够看出每当定义一个事件的时候,都会向$$listeners对象中添加以name为key的属性,值就是事件执行函数,注意这里有个事件计数,只要有父级,则也给父级的$$listenerCount添加以name为key的属性,而且值+1,这个$$listenerCount
会在广播事件的时候用到,最后这个方法返回一个取消事件的函数,先设置$$listeners中以name为key的值为null,而后调用decrementListenerCount来使该事件计数-1.
这个方法是用来触发$on定义的事件,原理就是loop$$listeners属性,检查是否有值,有的话,则执行,而后依次往上检查父级,这个方法有点相似冒泡执行事件.
$emit: function(name, args) {
var empty = [],
namedListeners,
scope = this,
stopPropagation = false,
event = {
name: name,
targetScope: scope,
stopPropagation: function() {stopPropagation = true;},
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
listenerArgs = concat([event], arguments, 1),
i, length;
do { namedListeners = scope.$$listeners[name] || empty; event.currentScope = scope; for (i=0, length=namedListeners.length; i<length; i++) { // if listeners were deregistered, defragment the array if (!namedListeners[i]) { namedListeners.splice(i, 1); i--; length--; continue; } try { //allow all listeners attached to the current scope to run namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) return event; //traverse upwards scope = scope.$parent; } while (scope); return event; }
上面的代码比较简单,首先定义一个事件参数,而后开启一个loop,只要scope有值,则一直执行,这个方法的事件链是一直向上传递的,不过当在事件函数执行stopPropagation方法,就会中止向上传递事件.
这个是$emit的升级版,广播事件,即能向上传递,也能向下传递,还能平级传递,核心原理就是利用深度遍历当前做用域
$broadcast: function(name, args) {
var target = this,
current = target,
next = target,
event = {
name: name,
targetScope: target,
preventDefault: function() {
event.defaultPrevented = true;
},
defaultPrevented: false
},
listenerArgs = concat([event], arguments, 1),
listeners, i, length;
//down while you can, then up and next sibling or up and next sibling until back at root while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; for (i=0, length = listeners.length; i<length; i++) { // if listeners were deregistered, defragment the array if (!listeners[i]) { listeners.splice(i, 1); i--; length--; continue; } try { listeners[i].apply(null, listenerArgs); } catch(e) { $exceptionHandler(e); } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest // (though it differs due to having the extra check for $$listenerCount) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } return event;
}
代码跟$emit差很少,只是跟它不一样的时,这个是不断的取next值,而next的值则是经过深度遍历它的子级节点,兄弟节点,父级节点,依次查找可用的以name为key的事件.注意这里的注释,跟$digest里的差很少,都是经过深度遍历查找,因此$broadcast方法也不能经常使用,性能不是很理想
这个方法是用来销毁当前做用域,代码主要是清空当前做用域内的一些实例属性,以避免执行digest,$emit,$broadcast时会关联到
$destroy: function() { // we can't destroy the root scope or a scope that has been already destroyed if (this.$$destroyed) return; var parent = this.$parent; this.$broadcast('$destroy'); this.$$destroyed = true; if (this === $rootScope) return; forEach(this.$$listenerCount, bind(null, decrementListenerCount, this)); // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; // All of the code below is bogus code that works around V8's memory leak via optimized code // and inline caches. // // see: // - https://code.google.com/p/v8/issues/detail?id=2073#c26 // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909 // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451 this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead = this.$$childTail = this.$root = null; // don't reset these to null in case some async task tries to register a listener/watch/task this.$$listeners = {}; this.$$watchers = this.$$asyncQueue = this.$$postDigestQueue = []; // prevent NPEs since these methods have references to properties we nulled out this.$destroy = this.$digest = this.$apply = noop; this.$on = this.$watch = function() { return noop; }; }
代码比较简单,先是经过foreach来清空$$listenerCount实例属性,而后再设置$parent,$$nextSibling,$$prevSibling,$$childHead,$$childTail,$root为null,清空$$listeners,$$watchers,$$asyncQueue,$$postDigestQueue,最后就是重罢方法为noop占位函数
rootScope说完了,这是个使用比例很是高的核心provider,分析的比较简单,有啥错误的地方,但愿你们可以指出来,你们一块儿学习学习,下次有空接着分析别的.
做者: feenan
本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。