簡介
在ng的生態中scope處於一個核心的地位,ng對外宣稱的雙向繫結的底層其實就是scope實現的,本章主要對scope的watch機制、繼承性以及事件的實現作下分析。
監聽
1. $watch
1.1 使用
// $watch: function(watchExp, listener, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
使用過angular的會經常這上面這樣的代碼,俗稱“手動”添加監聽,其他的一些都是通過插值或者directive自動地添加監聽,但是原理上都一樣。
1.2 源碼分析
function(watchExp, listener, objectEquality) { var scope = this, // 將可能的字串編譯成fn get = compileToFn(watchExp, 'watch'), array = scope.$$watchers, watcher = { fn: listener, last: initWatchVal, // 上次值記錄,方便下次比較 get: get, exp: watchExp, eq: !!objectEquality // 配置是引用比較還是值比較 }; lastDirtyWatch = null; if (!isFunction(listener)) { var listenFn = compileToFn(listener || noop, 'listener'); watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);}; } if (!array) { array = scope.$$watchers = []; } // 之所以使用unshift不是push是因為在 $digest 中watchers迴圈是從後開始 // 為了使得新加入的watcher也能在當次迴圈中執行所以放到隊列最前 array.unshift(watcher); // 返回unwatchFn, 取消監聽 return function deregisterWatch() { arrayRemove(array, watcher); lastDirtyWatch = null; };}
從代碼看 $watch 還是比較簡單,主要就是將 watcher 儲存到 $$watchers 數組中
2. $digest
當 scope 的值發生改變後,scope是不會自己去執行每個watcher的listenerFn,必須要有個通知,而發送這個通知的就是 $digest
2.1 源碼分析
整個 $digest 的源碼差不多100行,主體邏輯集中在【髒值檢查迴圈】(dirty check loop) 中, 迴圈後也有些次要的代碼,如 postDigestQueue 的處理等就不作詳細分析了。
髒值檢查迴圈,意思就是說只要還有一個 watcher 的值存在更新那麼就要運行一輪檢查,直到沒有值更新為止,當然為了減少不必要的檢查作了一些最佳化。
代碼:
// 進入$digest迴圈打上標記,防止重複進入beginPhase('$digest');lastDirtyWatch = null;// 髒值檢查迴圈開始do { dirty = false; current = target; // asyncQueue 迴圈省略 traverseScopesLoop: do { if ((watchers = current.$$watchers)) { length = watchers.length; while (length--) { try { watch = watchers[length]; if (watch) { // 作更新判斷,是否有值更新,分解如下 // value = watch.get(current), last = watch.last // value !== last 如果成立,則判斷是否需要作值判斷 watch.eq?equals(value, last) // 如果不是值相等判斷,則判斷 NaN的情況,即 NaN !== NaN 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; // 記錄這個迴圈中哪個watch發生改變 lastDirtyWatch = watch; // 緩衝last值 watch.last = watch.eq ? copy(value, null) : value; // 執行listenerFn(newValue, lastValue, scope) // 如果第一次執行,那麼 lastValue 也設定為newValue watch.fn(value, ((last === initWatchVal) ? value : last), current); // ... watchLog 省略 if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers}); } // 這邊就是減少watcher的最佳化 // 如果上個迴圈最後一個更新的watch沒有改變,即本輪也沒有新的有更新的watch // 那麼說明整個watches已經穩定不會有更新,本輪迴圈就此結束,剩下的watch就不用檢查了 else if (watch === lastDirtyWatch) { dirty = false; break traverseScopesLoop; } } } catch (e) { clearPhase(); $exceptionHandler(e); } } } // 這段有點繞,其實就是實現深度優先遍曆 // A->[B->D,C->E] // 執行順序 A,B,D,C,E // 每次優先擷取第一個child,如果沒有那麼擷取nextSibling兄弟,如果連兄弟都沒了,那麼後退到上一層並且判斷該層是否有兄弟,沒有的話繼續上退,直到退到開始的scope,這時next==null,所以會退出scopes的迴圈 if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // break traverseScopesLoop 直接到這邊 // 判斷是不是還處在髒值迴圈中,並且已經超過最大檢查次數 ttl預設10 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); // 迴圈結束// 標記退出digest迴圈clearPhase();
上述代碼中存在3層迴圈
第一層判斷 dirty,如果有髒值那麼繼續迴圈
do {
// ...
} while (dirty)
第二層判斷 scope 是否遍曆完畢,代碼翻譯了下,雖然還是繞但是能看懂
do {
// ....
if (current.$$childHead) {
next = current.$$childHead;
} else if (current !== target && current.$$nextSibling) {
next = current.$$nextSibling;
}
while (!next && current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
} while (current = next);
第三層迴圈scope的 watchers
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// ... 省略
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
3. $evalAsync
3.1 源碼分析
$evalAsync用於順延強制,源碼如下:
function(expr) { if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) { $browser.defer(function() { if ($rootScope.$$asyncQueue.length) { $rootScope.$digest(); } }); } this.$$asyncQueue.push({scope: this, expression: expr});}
通過判斷是否已經有 dirty check 在運行,或者已經有人觸發過$evalAsync
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)$browser.defer 就是通過調用 setTimeout 來達到改變執行順序 $browser.defer(function() { //... });
如果不是使用defer,那麼
function (exp) { queue.push({scope: this, expression: exp}); this.$digest();}scope.$evalAsync(fn1);scope.$evalAsync(fn2);// 這樣的結果是// $digest() > fn1 > $digest() > fn2// 但是實際需要達到的效果:$digest() > fn1 > fn2
上節 $digest 中省略了了async 的內容,位於第一層迴圈中
while(asyncQueue.length) { try { asyncTask = asyncQueue.shift(); asyncTask.scope.$eval(asyncTask.expression); } catch (e) { clearPhase(); $exceptionHandler(e); } lastDirtyWatch = null;}
簡單易懂,彈出asyncTask進行執行。
不過這邊有個細節,為什麼這麼設定呢?原因如下,假如在某次迴圈中執行到watchX時新加入1個asyncTask,此時會設定 lastDirtyWatch=watchX,恰好該task執行會導致watchX後續的一個watch執行出新值,如果沒有下面的代碼,那麼下個迴圈到 lastDirtyWatch (watchX)時便跳出迴圈,並且此時dirty==false。
lastDirtyWatch = null;
還有這邊還有一個細節,為什麼在第一層迴圈呢?因為具有繼承關係的scope其 $$asyncQueue 是公用的,都是掛載在root上,故不需要在下一層的scope層中執行。
2. 繼承性
scope具有繼承性,如 $parentScope, $childScope 兩個scope,當調用 $childScope.fn 時如果 $childScope 中沒有 fn 這個方法,那麼就是去 $parentScope上尋找該方法。
這樣一層層往上尋找直到找到需要的屬性。這個特性是利用 javascirpt 的原型繼承的特點實現。
源碼:
function(isolate) { var ChildScope, child; if (isolate) { child = new Scope(); child.$root = this.$root; // isolate 的 asyncQueue 及 postDigestQueue 也都是公用root的,其他獨立 child.$$asyncQueue = this.$$asyncQueue; child.$$postDigestQueue = this.$$postDigestQueue; } else { if (!this.$$childScopeClass) { this.$$childScopeClass = function() { // 這裡可以看出哪些屬性是隔離專屬的,如$$watchers, 這樣就獨立監聽了, this.$$watchers = this.$$nextSibling = this.$$childHead = this.$$childTail = null; this.$$listeners = {}; this.$$listenerCount = {}; this.$id = nextUid(); this.$$childScopeClass = null; }; this.$$childScopeClass.prototype = this; } child = new this.$$childScopeClass(); } // 設定各種父子,兄弟關係,很亂! child['this'] = child; child.$parent = this; child.$$prevSibling = this.$$childTail; if (this.$$childHead) { this.$$childTail.$$nextSibling = child; this.$$childTail = child; } else { this.$$childHead = this.$$childTail = child; } return child;}
代碼還算清楚,主要的細節是哪些屬性需要獨立,哪些需要基礎下來。
最重要的代碼:
this.$$childScopeClass.prototype = this;
就這樣實現了繼承。
3. 事件機制
3.1 $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); };}
跟 $wathc 類似,也是存放到數組 -- namedListeners。
還有不一樣的地方就是該scope和所有parent都儲存了一個事件的統計數,廣播事件時有用,後續分析。
var current = this;do { if (!current.$$listenerCount[name]) { current.$$listenerCount[name] = 0; } current.$$listenerCount[name]++;} while ((current = current.$parent));
3.2 $emit
$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++) { // 當監聽remove以後,不會從數組中刪除,而是設定為null,所以需要判斷 if (!namedListeners[i]) { namedListeners.splice(i, 1); i--; length--; continue; } try { namedListeners[i].apply(null, listenerArgs); } catch (e) { $exceptionHandler(e); } } // 停止傳播時return if (stopPropagation) { event.currentScope = null; return event; } // emit是向上的傳播方式 scope = scope.$parent; } while (scope); event.currentScope = null; return event;}
3.3 $broadcast
$broadcast 是向內傳播,即向child傳播,源碼:
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; while ((current = next)) { event.currentScope = current; listeners = current.$$listeners[name] || []; for (i=0, length = listeners.length; i<length; i++) { // 檢查是否已經取消監聽了 if (!listeners[i]) { listeners.splice(i, 1); i--; length--; continue; } try { listeners[i].apply(null, listenerArgs); } catch(e) { $exceptionHandler(e); } } // 在digest中已經有過了 if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } event.currentScope = null; return event;}
其他邏輯比較簡單,就是在深度遍曆的那段代碼比較繞,其實跟digest中的一樣,就是多了在路徑上判斷是否有監聽,current.$$listenerCount[name],從上面$on的代碼可知,只要路徑上存在child有監聽,那麼該路徑頭也是有數位,相反如果沒有說明該路徑上所有child都沒有監聽事件。
if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while(current !== target && !(next = current.$$nextSibling)) { current = current.$parent; }}
傳播路徑:
Root>[A>[a1,a2], B>[b1,b2>[c1,c2],b3]]
Root > A > a1 > a2 > B > b1 > b2 > c1 > c2 > b3
4. $watchCollection
4.1 使用樣本
$scope.names = ['igor', 'matias', 'misko', 'james'];$scope.dataCount = 4;$scope.$watchCollection('names', function(newNames, oldNames) { $scope.dataCount = newNames.length;});expect($scope.dataCount).toEqual(4);$scope.$digest();expect($scope.dataCount).toEqual(4);$scope.names.pop();$scope.$digest();expect($scope.dataCount).toEqual(3);
4.2 源碼分析
function(obj, listener) { $watchCollectionInterceptor.$stateful = true; var self = this; var newValue; var oldValue; var veryOldValue; var trackVeryOldValue = (listener.length > 1); var changeDetected = 0; var changeDetector = $parse(obj, $watchCollectionInterceptor); var internalArray = []; var internalObject = {}; var initRun = true; var oldLength = 0; // 根據返回的changeDetected判斷是否變化 function $watchCollectionInterceptor(_value) { // ... return changeDetected; } // 通過此方法調用真正的listener,作為代理 function $watchCollectionAction() { } return this.$watch(changeDetector, $watchCollectionAction);}
主脈絡就是上面截取的部分代碼,下面主要分析 $watchCollectionInterceptor 和 $watchCollectionAction
4.3 $watchCollectionInterceptor
function $watchCollectionInterceptor(_value) { newValue = _value; var newLength, key, bothNaN, newItem, oldItem; if (isUndefined(newValue)) return; if (!isObject(newValue)) { if (oldValue !== newValue) { oldValue = newValue; changeDetected++; } } else if (isArrayLike(newValue)) { if (oldValue !== internalArray) { oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++; } newLength = newValue.length; if (oldLength !== newLength) { changeDetected++; oldValue.length = oldLength = newLength; } for (var i = 0; i < newLength; i++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; } } } else { if (oldValue !== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++; } newLength = 0; for (key in newValue) { if (hasOwnProperty.call(newValue, key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; if (key in oldValue) { bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[key] = newItem; } } else { oldLength++; oldValue[key] = newItem; changeDetected++; } } } if (oldLength > newLength) { changeDetected++; for (key in oldValue) { if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } } } } return changeDetected;}
1). 當值為undefined時直接返回。
2). 當值為普通基本類型時 直接判斷是否相等。
3). 當值為類數組 (即存在 length 屬性,並且 value[i] 也成立稱為類數組),先沒有初始化先初始化oldValue
if (oldValue !== internalArray) { oldValue = internalArray; oldLength = oldValue.length = 0; changeDetected++;}
然後比較數組長度,不等的話記為已變化 changeDetected++
if (oldLength !== newLength) { changeDetected++; oldValue.length = oldLength = newLength;}
再進行逐個比較
for (var i = 0; i < newLength; i++) { oldItem = oldValue[i]; newItem = newValue[i]; bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[i] = newItem; }}
4). 當值為object時,類似上面進行初始化處理
if (oldValue !== internalObject) { oldValue = internalObject = {}; oldLength = 0; changeDetected++;}
接下來的處理比較有技巧,但凡發現 newValue 多的新欄位,就在oldLength 加1,這樣 oldLength 只加不減,很容易發現 newValue 中是否有新欄位出現,最後把 oldValue中多出來的欄位也就是 newValue 中刪除的欄位給移除就結束了。
newLength = 0;for (key in newValue) { if (hasOwnProperty.call(newValue, key)) { newLength++; newItem = newValue[key]; oldItem = oldValue[key]; if (key in oldValue) { bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; oldValue[key] = newItem; } } else { oldLength++; oldValue[key] = newItem; changeDetected++; } }}if (oldLength > newLength) { changeDetected++; for (key in oldValue) { if (!hasOwnProperty.call(newValue, key)) { oldLength--; delete oldValue[key]; } }}
4.4 $watchCollectionAction
function $watchCollectionAction() { if (initRun) { initRun = false; listener(newValue, newValue, self); } else { listener(newValue, veryOldValue, self); } // trackVeryOldValue = (listener.length > 1) 查看listener方法是否需要oldValue // 如果需要就進行複製 if (trackVeryOldValue) { if (!isObject(newValue)) { veryOldValue = newValue; } else if (isArrayLike(newValue)) { veryOldValue = new Array(newValue.length); for (var i = 0; i < newValue.length; i++) { veryOldValue[i] = newValue[i]; } } else { veryOldValue = {}; for (var key in newValue) { if (hasOwnProperty.call(newValue, key)) { veryOldValue[key] = newValue[key]; } } } }}
代碼還是比較簡單,就是調用 listenerFn,初次調用時 oldValue == newValue,為了效率和記憶體判斷了下 listener是否需要oldValue參數
5. $eval & $apply
$eval: function(expr, locals) { return $parse(expr)(this, locals);},$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 最後調用 $rootScope.$digest(),所以很多書上建議使用 $digest() ,而不是調用 $apply(),效率要高點。
主要邏輯都在$parse 屬於文法解析功能,後續單獨分析。