本文接着上一篇讲
回顾
上次说到了rootScope
里的$watch
方法中的解析监控表达式,即而引出了对parse
的分析,今天我们接着这里继续挖代码.
$watch续
先上一块$watch
代码
$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
对象,用来保存一些监听相关的信息,简单的说明一下
- fn, 代表监听函数,当监控表达式新旧不相等时会执行此函数
- last, 保存最后一次发生变化的监控表达式的值
- get, 保存一个监控表达式对应的函数,目的是用来获取表达式的值然后用来进行新旧对比的
- exp, 保存一个原始的监控表达式
- eq, 保存
$watch
函数的第三个参数,表示是否进行深度比较
然后会检查传递进来的监听参数是否为函数,如果是一个有效的字符串,则通过parse
来解析生成一个函数,否则赋值为一个noop
占位函数,最后生成一个包装函数,函数体的内容就是执行刚才生成的监听函数,默认传递当前作用域.
接着会检查监控表达式是否为字符串并且执行表达式的constant
为true,代表这个字符串是一个常量,那么,系统在处理这种监听的时候,执行完一次监听函数之后就会删除这个$watch
.最后往当前作用域里的$$watchers
数组头中添加$watch
信息,注意这里的返回值,利用JS的闭包保留了当前的watcher
,然后返回一个函数,这个就是用来删除监听用的.
$eval
这个$eval
也是挺方便的函数,假如你想直接在程序里执行一个字符串的话,那么可以这么用
$scope.name = '2';
$scope.$eval('1+name'); // ==> 会输出12
大家来看看它的函数体
return $parse(expr)(this, locals);
其实就是通过parse
来解析成一个执行表达式函数,然后传递当前作用域以及额外的参数,返回这个执行表达式函数的值
$evalAsync
evalAsync
函数的作用就是延迟执行表达式,并且执行完不管是否异常,触发dirty check
.
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
方法之前,还有一个方法要说说
$postDigest
这个方法跟evalAsync
不同的时,它不会主动触发digest
方法,只是往postDigestQueue
队列中增加执行表达式,它会在digest
体内最后执行,相当于在触发dirty check
之后,可以执行别的一些逻辑.
this.$$postDigestQueue.push(fn);
下面我们来重点说说digest
方法
$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
方法
$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
,这三个很实用的方法
$on
这个方法是用来定义事件的,这里用到了两个实例变量$$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
.
$emit
这个方法是用来触发$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
方法,就会停止向上传递事件.
$broadcast
这个是$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
方法也不能常用,性能不是很理想
$destroy
这个方法是用来销毁当前作用域,代码主要是清空当前作用域内的一些实例属性,以免执行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
,分析的比较简单,有啥错误的地方,希望大家能够指出来,大家一起学习学习,下次有空接着分析别的.