AngularJS 源碼分析3

本文接着上一篇講

上一篇地址html


回顧

上次說到了rootScope裏的$watch方法中的解析監控表達式,即而引出了對parse的分析,今天咱們接着這裏繼續挖代碼.git

$watch續

先上一塊$watch代碼github

$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對象,用來保存一些監聽相關的信息,簡單的說明一下express

  • fn, 表明監聽函數,當監控表達式新舊不相等時會執行此函數
  • last, 保存最後一次發生變化的監控表達式的值
  • get, 保存一個監控表達式對應的函數,目的是用來獲取表達式的值而後用來進行新舊對比的
  • exp, 保存一個原始的監控表達式
  • eq, 保存$watch函數的第三個參數,表示是否進行深度比較

而後會檢查傳遞進來的監聽參數是否爲函數,若是是一個有效的字符串,則經過parse來解析生成一個函數,不然賦值爲一個noop佔位函數,最後生成一個包裝函數,函數體的內容就是執行剛纔生成的監聽函數,默認傳遞當前做用域.api

接着會檢查監控表達式是否爲字符串而且執行表達式的constant爲true,表明這個字符串是一個常量,那麼,系統在處理這種監聽的時候,執行完一次監聽函數以後就會刪除這個$watch.最後往當前做用域裏的$$watchers數組頭中添加$watch信息,注意這裏的返回值,利用JS的閉包保留了當前的watcher,而後返回一個函數,這個就是用來刪除監聽用的.數組

$eval

這個$eval也是挺方便的函數,假如你想直接在程序裏執行一個字符串的話,那麼能夠這麼用閉包

$scope.name = '2';
$scope.$eval('1+name'); // ==> 會輸出12

你們來看看它的函數體app

return $parse(expr)(this, locals);

其實就是經過parse來解析成一個執行表達式函數,而後傳遞當前做用域以及額外的參數,返回這個執行表達式函數的值異步

$evalAsync

evalAsync函數的做用就是延遲執行表達式,而且執行完不論是否異常,觸發dirty check.async

 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

出處: http://www.cnblogs.com/xuwenmin888

本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索