angularjs 源碼解析之scope
簡介
在ng的生態(tài)中scope處于一個核心的地位,ng對外宣稱的雙向綁定的底層其實就是scope實現(xiàn)的,本章主要對scope的watch機制、繼承性以及事件的實現(xiàn)作下分析。
監(jiān)聽
1. $watch
1.1 使用
// $watch: function(watchExp, listener, objectEquality)
var unwatch = $scope.$watch('aa', function () {}, isEqual);
使用過angular的會經(jīng)常這上面這樣的代碼,俗稱“手動”添加監(jiān)聽,其他的一些都是通過插值或者directive自動地添加監(jiān)聽,但是原理上都一樣。
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循環(huán)是從后開始
// 為了使得新加入的watcher也能在當次循環(huán)中執(zhí)行所以放到隊列最前
array.unshift(watcher);
// 返回unwatchFn, 取消監(jiān)聽
return function deregisterWatch() {
arrayRemove(array, watcher);
lastDirtyWatch = null;
};
}
從代碼看 $watch 還是比較簡單,主要就是將 watcher 保存到 $$watchers 數(shù)組中
2. $digest
當 scope 的值發(fā)生改變后,scope是不會自己去執(zhí)行每個watcher的listenerFn,必須要有個通知,而發(fā)送這個通知的就是 $digest
2.1 源碼分析
整個 $digest 的源碼差不多100行,主體邏輯集中在【臟值檢查循環(huán)】(dirty check loop) 中, 循環(huán)后也有些次要的代碼,如 postDigestQueue 的處理等就不作詳細分析了。
臟值檢查循環(huán),意思就是說只要還有一個 watcher 的值存在更新那么就要運行一輪檢查,直到?jīng)]有值更新為止,當然為了減少不必要的檢查作了一些優(yōu)化。
代碼:
// 進入$digest循環(huán)打上標記,防止重復進入
beginPhase('$digest');
lastDirtyWatch = null;
// 臟值檢查循環(huán)開始
do {
dirty = false;
current = target;
// asyncQueue 循環(huán)省略
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;
// 記錄這個循環(huán)中哪個watch發(fā)生改變
lastDirtyWatch = watch;
// 緩存last值
watch.last = watch.eq ? copy(value, null) : value;
// 執(zhí)行l(wèi)istenerFn(newValue, lastValue, scope)
// 如果第一次執(zhí)行,那么 lastValue 也設置為newValue
watch.fn(value, ((last === initWatchVal) ? value : last), current);
// ... watchLog 省略
if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers});
}
// 這邊就是減少watcher的優(yōu)化
// 如果上個循環(huán)最后一個更新的watch沒有改變,即本輪也沒有新的有更新的watch
// 那么說明整個watches已經(jīng)穩(wěn)定不會有更新,本輪循環(huán)就此結(jié)束,剩下的watch就不用檢查了
else if (watch === lastDirtyWatch) {
dirty = false;
break traverseScopesLoop;
}
}
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
}
// 這段有點繞,其實就是實現(xiàn)深度優(yōu)先遍歷
// A->[B->D,C->E]
// 執(zhí)行順序 A,B,D,C,E
// 每次優(yōu)先獲取第一個child,如果沒有那么獲取nextSibling兄弟,如果連兄弟都沒了,那么后退到上一層并且判斷該層是否有兄弟,沒有的話繼續(xù)上退,直到退到開始的scope,這時next==null,所以會退出scopes的循環(huán)
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while ((current = next));
// break traverseScopesLoop 直接到這邊
// 判斷是不是還處在臟值循環(huán)中,并且已經(jīng)超過最大檢查次數(shù) 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); // 循環(huán)結(jié)束
// 標記退出digest循環(huán)
clearPhase();
上述代碼中存在3層循環(huán)
第一層判斷 dirty,如果有臟值那么繼續(xù)循環(huán)
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);
第三層循環(huán)scope的 watchers
length = watchers.length;
while (length--) {
try {
watch = watchers[length];
// ... 省略
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
}
3. $evalAsync
3.1 源碼分析
$evalAsync用于延遲執(zhí)行,源碼如下:
function(expr) {
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length) {
$browser.defer(function() {
if ($rootScope.$$asyncQueue.length) {
$rootScope.$digest();
}
});
}
this.$$asyncQueue.push({scope: this, expression: expr});
}
通過判斷是否已經(jīng)有 dirty check 在運行,或者已經(jīng)有人觸發(fā)過$evalAsync
if (!$rootScope.$$phase && !$rootScope.$$asyncQueue.length)
$browser.defer 就是通過調(diào)用 setTimeout 來達到改變執(zhí)行順序
$browser.defer(function() {
//...
});
如果不是使用defer,那么
function (exp) {
queue.push({scope: this, expression: exp});
this.$digest();
}
scope.$evalAsync(fn1);
scope.$evalAsync(fn2);
// 這樣的結(jié)果是
// $digest() > fn1 > $digest() > fn2
// 但是實際需要達到的效果:$digest() > fn1 > fn2
上節(jié) $digest 中省略了了async 的內(nèi)容,位于第一層循環(huán)中
while(asyncQueue.length) {
try {
asyncTask = asyncQueue.shift();
asyncTask.scope.$eval(asyncTask.expression);
} catch (e) {
clearPhase();
$exceptionHandler(e);
}
lastDirtyWatch = null;
}
簡單易懂,彈出asyncTask進行執(zhí)行。
不過這邊有個細節(jié),為什么這么設置呢?原因如下,假如在某次循環(huán)中執(zhí)行到watchX時新加入1個asyncTask,此時會設置 lastDirtyWatch=watchX,恰好該task執(zhí)行會導致watchX后續(xù)的一個watch執(zhí)行出新值,如果沒有下面的代碼,那么下個循環(huán)到 lastDirtyWatch (watchX)時便跳出循環(huán),并且此時dirty==false。
lastDirtyWatch = null;
還有這邊還有一個細節(jié),為什么在第一層循環(huán)呢?因為具有繼承關(guān)系的scope其 $$asyncQueue 是公用的,都是掛載在root上,故不需要在下一層的scope層中執(zhí)行。
2. 繼承性
scope具有繼承性,如 $parentScope, $childScope 兩個scope,當調(diào)用 $childScope.fn 時如果 $childScope 中沒有 fn 這個方法,那么就是去 $parentScope上查找該方法。
這樣一層層往上查找直到找到需要的屬性。這個特性是利用 javascirpt 的原型繼承的特點實現(xiàn)。
源碼:
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, 這樣就獨立監(jiān)聽了,
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();
}
// 設置各種父子,兄弟關(guān)系,很亂!
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;
}
代碼還算清楚,主要的細節(jié)是哪些屬性需要獨立,哪些需要基礎下來。
最重要的代碼:
this.$$childScopeClass.prototype = this;
就這樣實現(xiàn)了繼承。
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 類似,也是存放到數(shù)組 -- namedListeners。
還有不一樣的地方就是該scope和所有parent都保存了一個事件的統(tǒng)計數(shù),廣播事件時有用,后續(xù)分析。
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++) {
// 當監(jiān)聽remove以后,不會從數(shù)組中刪除,而是設置為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 是向內(nèi)傳播,即向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++) {
// 檢查是否已經(jīng)取消監(jiān)聽了
if (!listeners[i]) {
listeners.splice(i, 1);
i--;
length--;
continue;
}
try {
listeners[i].apply(null, listenerArgs);
} catch(e) {
$exceptionHandler(e);
}
}
// 在digest中已經(jīng)有過了
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中的一樣,就是多了在路徑上判斷是否有監(jiān)聽,current.$$listenerCount[name],從上面$on的代碼可知,只要路徑上存在child有監(jiān)聽,那么該路徑頭也是有數(shù)字的,相反如果沒有說明該路徑上所有child都沒有監(jiān)聽事件。
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;
// 根據(jù)返回的changeDetected判斷是否變化
function $watchCollectionInterceptor(_value) {
// ...
return changeDetected;
}
// 通過此方法調(diào)用真正的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). 當值為類數(shù)組 (即存在 length 屬性,并且 value[i] 也成立稱為類數(shù)組),先沒有初始化先初始化oldValue
if (oldValue !== internalArray) {
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
然后比較數(shù)組長度,不等的話記為已變化 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++;
}
接下來的處理比較有技巧,但凡發(fā)現(xiàn) newValue 多的新字段,就在oldLength 加1,這樣 oldLength 只加不減,很容易發(fā)現(xiàn) newValue 中是否有新字段出現(xiàn),最后把 oldValue中多出來的字段也就是 newValue 中刪除的字段給移除就結(jié)束了。
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];
}
}
}
}
}
代碼還是比較簡單,就是調(diào)用 listenerFn,初次調(diào)用時 oldValue == newValue,為了效率和內(nèi)存判斷了下 listener是否需要oldValue參數(shù)
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 最后調(diào)用 $rootScope.$digest(),所以很多書上建議使用 $digest() ,而不是調(diào)用 $apply(),效率要高點。
主要邏輯都在$parse 屬于語法解析功能,后續(xù)單獨分析。
相關(guān)文章
Angular Excel 導入與導出的實現(xiàn)代碼
這篇文章主要介紹了Angular Excel 導入與導出的實現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04
AngularJS監(jiān)聽ng-repeat渲染完成的兩種方法
這篇文章主要介紹了AngularJS監(jiān)聽ng-repeat渲染完成的兩種方法,結(jié)合實例形式分析了AngularJS基于自定義指令及廣播事件實現(xiàn)監(jiān)聽功能的相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
Angular將填入表單的數(shù)據(jù)渲染到表格的方法
這篇文章主要介紹了Angular將填入表單的數(shù)據(jù)渲染到表格的方法,非常具有實用價值,需要的朋友可以參考下2017-09-09

