angularjs 源碼解析之scope_AngularJS

來源:互聯網
上載者:User

簡介

在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 屬於文法解析功能,後續單獨分析。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.