AngularJS源碼分析之{{雙向數據綁定}}

文章參考html

我的博客: www.morphzhou.cn數組

0x00 簡單囉嗦點

所謂雙向數據綁定,概念上爲數據模型到視圖的綁定,以及視圖到數據模型的綁定。容易理解的說法就是從界面的操做能實時反映到數據,數據的變動能實時展示到界面。好比Angular中的一個雙向數據綁定的示例:app

VU5EACPPPQ3DWVDM.png

{{yourname}}經過ng-model與input的value綁定,當input的value改變的時候<h1>內的值就會相應改變async

6WNKJZC48LUKFBYIH.png

雙向數據綁定的優勢是無需進行和單向數據綁定的那些CRUD(Create,Retrieve,Update,Delete)操做函數

0x01 雙向數據綁定實現機制

目前對於雙向數據綁定的實現有這麼幾種流派oop

  • 髒值檢測,例如AngularJS源碼分析

  • Getter/Setter,例如Vue.jspost

對於Getter/Setter實現的數據雙向綁定來講,核心在於重定義model的getter與setter方法,在數據變更的時候從新渲染頁面。兩種方式各有優劣。

當咱們使用Getter/Setter的時候,每次修改數值都會激活刷新模版的方法,而髒值檢測則能夠在完成全部數值變更後,統一刷新到Dom。可是當監聽元素變多的時候,watcher列表會變得很長,查詢變更的數據元素將耗費更多的資源。

0x02 AngularJS雙向數據綁定源碼分析

源碼版本 Angular-1.5.0 angular.js
在Angular當中,有個貫穿始終的對象$scope。Scope本質爲一個構造函數,而$scope就是Scope的實例。源碼16028行

function Scope() {
      this.$id = nextUid();
      this.$$phase = this.$parent = this.$$watchers =
                     this.$$nextSibling = this.$$prevSibling =
                     this.$$childHead = this.$$childTail = null;
      this.$root = this;
      this.$$destroyed = false;
      this.$$listeners = {};
      this.$$listenerCount = {};
      this.$$watchersCount = 0;
      this.$$isolateBindings = null;
}

在Scope的原型(Scope.prototype)中共定義了13個函數。其中有兩個函數對雙向數據綁定起着相當重要的做用:監視對象屬性

  • $watch

  • $digest

$watch$digest是同一個硬幣的兩面。它們兩者同時造成了$digest循環的核心:對數據的變化作出反應。可使用$watch函數爲scope添加一個監視器。當這個scope中有變化發生時,監視器便會提醒你。

$watch 源碼16247行

$watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
        var get = $parse(watchExp);
 
        if (get.$$watchDelegate) {
          return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
        }
        var scope = this,
            array = scope.$$watchers,
            watcher = {
              fn: listener,
              last: initWatchVal,
              get: get,
              exp: prettyPrintExpression || watchExp,
              eq: !!objectEquality
            };
 
        lastDirtyWatch = null;
 
        if (!isFunction(listener)) {
          watcher.fn = noop;
        }
 
        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;
        };
}

爲了監視一個變量的變化,可使用$scope.$watch函數。這個函數的前兩個,它指明瞭要觀察什麼(watchExp),在變化時要發生什麼(listener)。

$scope.name = 'Morph_Zhou';
 
$scope.$watch( function( ) {
    return $scope.name;
}, function( newValue, oldValue ) {
    console.log('$scope.name was updated!');
} );

在Scope中有一個對象數組$$watchers,裏面保存着咱們定義的全部的監視器對象watcher$watch函數將會返回一個deregisterWatch函數。這意味着若是咱們使用$scope.$watch對一個變量進行監視,咱們也能夠在之後經過調用某個函數來中止監視。

另一個是$digest函數。它迭代了全部綁定到scope中的監視器,而後進行監視並運行相應的監聽函數。

$digest 源碼16607行

$digest: function() {
        var watch, value, last, fn, get,
            watchers,
            length,
            dirty, ttl = TTL,
            next, current, target = this,
            watchLog = [],
            logIdx, logMsg, asyncTask;
 
        beginPhase('$digest');
        // Check for changes to browser url that happened in sync before the call to $digest
        $browser.$$checkUrlChange();
 
        if (this === $rootScope && applyAsyncId !== null) {
          // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
          // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
          $browser.defer.cancel(applyAsyncId);
          flushApplyAsync();
        }
 
        lastDirtyWatch = null;
 
        do { // "while dirty" loop
          dirty = false;
          current = target;
 
          while (asyncQueue.length) {
            try {
              asyncTask = asyncQueue.shift();
              asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
            } catch (e) {
              $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) {
                    get = watch.get;
                    if ((value = 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, null) : value;
                      fn = watch.fn;
                      fn(value, ((last === initWatchVal) ? value : last), current);
                      if (ttl < 5) {
                        logIdx = 4 - ttl;
                        if (!watchLog[logIdx]) watchLog[logIdx] = [];
                        watchLog[logIdx].push({
                          msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp,
                          newVal: value,
                          oldVal: last
                        });
                      }
                    } 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) {
                  $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.$$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 || asyncQueue.length);
 
        clearPhase();
 
        while (postDigestQueue.length) {
          try {
            postDigestQueue.shift()();
          } catch (e) {
            $exceptionHandler(e);
          }
        }
}

$digest函數將會在$rootScope中被$scope.$apply所調用。它將會在$rootScope中運行digest循環,而後向下遍歷每個做用域並在每一個做用域上運行循環。在簡單的情形中,digest循環將會觸發全部位於$$watchers變量中的全部watchExp函數,將它們和最新的值進行對比,若是值不相同,就會觸發監聽器。當digest循環運行時,它將會遍歷全部的監聽器而後再次循環,只要此次循環發現了」髒值」,循環就會繼續下去。若是watchExp的值和最新的值不相同,那麼此次循環就會被認爲發現了「髒值」。

0x03 實現本身的雙向數據綁定

實際上雙向數據綁定的功能遠遠不止這麼一些,這裏僅僅是極盡簡化的版本。若是想實現一個功能較爲齊全的,能夠參考慕課網上大漠窮秋的一節課程當中的要求。

A@B2W7X3PAWBAPPRXQQ2.png

首先咱們先要模仿Angular設置本身的scope,咱們只須要簡單的實現一下$watch,以及$digest方法。$watch函數須要接受兩個參數,watchExplistener。當$watch被調用時,咱們須要將它們push進入到Scope的$$watcher數組中。若是沒有提供listener,咱們會將listener設置爲一個空函數,這樣一來咱們能夠$watch全部的變量。接下來咱們將會建立$digest。咱們須要來檢查舊值是否等於新的值,若是兩者不相等,監聽器就會被觸發。咱們會一直循環這個過程,直到兩者相等。

var Scope = function() {
    this.$$watchers = [];
};
 
Scope.prototype.$watch = function(watchExp, listener) {
    this.$$watchers.push({
        watchExp: watchExp,
        listener: listener || function() {}
    });
};
 
Scope.prototype.$digest = function() {
    var dirty;
 
    do {
        dirty = false;
 
        for (var i = 0; i &lt; this.$$watchers.length; i++) {
            var newValue = this.$$watchers[i].watchExp(),
                oldValue = this.$$watchers[i].last;
 
            if (oldValue !== newValue) {
                this.$$watchers[i].listener(newValue, oldValue);
 
                dirty = true;
 
                this.$$watchers[i].last = newValue;
            }
        }
    } while (dirty);
};

若是咱們把$digest函數綁定到一個input元素的keyup事件上。

var $scope = new Scope();
 
$scope.name = 'Morph_Zhou';
 
var element = document.querySelectorAll("input");
 
element[0].onkeyup = function() {
    $scope.name = element[0].value;
 
    $scope.$digest();
};
 
$scope.$watch(function() {
    return $scope.name;
}, function(newValue, oldValue) {
    console.log('Input value updated - it is now ' + newValue);
 
    element[0].value = $scope.name;
});
 
var updateScopeValue = function updateScopeValue() {
    $scope.name = 'Morph_Gaming';
    $scope.$digest();
};

使用上面的代碼,不管什麼時候咱們改變了input的值,$scope中的name屬性都會相應的發生變化。

@6YL6N8KK4PW28@HAP12.png

相關文章
相關標籤/搜索