本文接着上一篇講
回顧
上次說到了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,分析的比較簡單,有啥錯誤的地方,希望大家能夠指出來,大家一起學習學習,下次有空接着分析別的.
作者聲明
作者: feenan
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
