AngularJS 的髒值檢查和事件機制

wxlworkhard發表於2016-11-02

AngularJS 的髒值檢查和事件機制都是基於作用域實現的,該文章沒有涉及的作用域的建立、銷燬等和生命週期相關的內容,只是把髒值檢查和事件機制拆出來講解一下。

本文引用的 AngularJS 的原始碼略去了很多程式碼,有需要可以直接看原始碼。

髒值檢查

在看該部分前強烈建議看下這篇文章(連結 http://www.ituring.com.cn/article/39865)對髒值檢查的實現講解很詳細,本文略去了一些不太關心的細節,只關注主要的實現原理。

AngularJS 的髒值檢查的實現主要基於作用域 $scope 的 $$watchers 屬性、$watch 和 $digest 方法,是 Sub/Pub 模式的一種實現。

$watch

function Scope() {
    this.$$watchersCount = 0;
    this.$$watchers = [];
}
Scope.prototype.$watch = function(watchExp, listener, objectEquality) {
    // 利用依賴注入的 $parse 服務轉換成函式,用於獲取作用域裡的變數值
    var get = $parse(watchExp); 
    var scope = this,
         array = scope.$$watchers,
         watcher = {
             fn: listener,              // 值變化後需要執行的偵聽函式
             last: initWatchVal,    // 最新的值
             get: get,
             eq: !!objectEquality  // 是否進行嚴格匹配
         };

    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);
    incrementWatchersCount(this, 1);

    return function deregisterWatch() {
        if (arrayRemove(array, watcher) >= 0) {
            incrementWatchersCount(scope, -1);
        }
        lastDirtyWatch = null;
    };
};

$watch 是 Sub 部分,主要是往 scope 物件的 $$watchers 裡面新增 watcher 物件,注意 $$watchersCount 記錄了當前作用域和其子作用域的 watchers 的總數。

$digest

Scope.prototype.$digest = function() {
    var watch, value, last, fn, get,
        watchers,
        length,
        dirty, ttl = TTL,
        next, current, target = this;

    beginPhase('$digest');

    // 外層 loop 在 dirty 為 true 時執行
    do { 
        dirty = false;
        current = target;

        // 內層 loop 遍歷作用域樹的所有作用域,在值改變時執行對應的 fn
        do { 
            if ((watchers = current.$$watchers)) {
                length = watchers.length;

                // 在 $watch 中使用 unshift 新增物件的原因                    
                while (length--) {
                    watch = watchers[length];
                    if (watch) {
                        get = watch.get;

                        // 判斷值是否改變這裡涉及到物件、陣列、NaN 等的比較,這裡略去程式碼
                        if (......) {
                            dirty = true;
                            watch.last = watch.eq ? copy(value, null) : value;
                            fn = watch.fn;
                            fn(value, ......);
                        } else if (watch === lastDirtyWatch) {
                            dirty = false;
                            break traverseScopesLoop;
                        }
                    }
                }
            }

            // 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.$$watchersCount && 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, watchLog);
        }
    } while (dirty);
    clearPhase();
};

$digest 的實現主要是兩層巢狀的 while 迴圈,外層迴圈在 dirty 為 true 時執行。內層迴圈又包含了兩個 while 迴圈,第一個迴圈去遍歷某一個作用域物件的 watchers 陣列,判斷值是否改變,若改變執行 fn,記錄 last 值並設定 dirty 為 true;第二個迴圈使用深度優先的方法遍歷作用域樹(每個作用域物件都對應一個指令,指令又對映 DOM,DOM 是樹形結構,作用域也以樹形結構實現),獲取 next 作用域;這樣內層迴圈就遍歷了所有的作用域(因為 $digest 只能由 $rootScope 呼叫,$rootScope 是作用域樹的根節點)

事件(訊息)機制

AngularJS 的指令可以理解成擴充套件的 HTML,每個指令都會注入一個作用域,指令之間的通訊是通過作用域的通訊實現,作用域的事件機制是用釋出訂閱模式實現的,主要的 API 有 $on、$emit、$broadcast。

$on

function Scope() {
    this.$$listeners = {};
    this.$$listenerCount = {};
}

Scope.prototype.$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));
};

$on 是 Sub 部分,給當前作用域的事件偵聽陣列新增函式,並在其祖先元素上計數(在 $broadcast 時可以在該 count 不存在時直接返回而不必做作用域樹的深度優先的遍歷)

$emit

Scope.prototype.$emit = function(name, args) {
    var scope = this;
    //......
    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;
            }
            namedListeners[i].apply(null, listenerArgs);
        }

        //if any listener on the current scope stops propagation, prevent bubbling
        if (stopPropagation) {
            // 為什麼不直接 break
            event.currentScope = null;
            return event;
        }
        //traverse upwards
        scope = scope.$parent;
    } while (scope);

    event.currentScope = null;

    return event;
};

$emit 方法是往上拋事件,主要程式碼邏輯是迴圈取某個作用域的父級作用域,把取到的每個作用域物件的 namedListeners 陣列迴圈執行,提供了阻止“冒泡”(借用 DOM 的概念)的 API。

$broadcast

Scope.prototype.$broadcast = function(name, args) {
    var target = this,
        current = target,
        next = target;

    if (!target.$$listenerCount[name]) return event;

    var 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;
            }

          listeners[i].apply(null, listenerArgs);
        }

        // 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;
            }
        }
    }

    event.currentScope = null;
    return event;
};

$broadcast 往下級拋事件,又使用了深度優先遍歷作用域樹的方式獲取所有的子級作用域物件,執行回撥佇列,注意該方法不能阻止冒泡。

相關文章