AngularJs 雙向繫結機制解析

來源:互聯網
上載者:User

AngularJs 雙向繫結機制解析
AngularJs 的元素與模型雙向繫結依賴於迴圈檢測它們之間的值,這種做法叫做髒檢測,這幾天研究了一下其源碼,將 Angular 的實現分享一下。 首先看看如何將 Model 的變更更新到 UI Angular 的 Model 是一個 Scope 的類型,每個 Scope 都歸屬於一個 Directive 對象,比如 $rootScope 就歸屬於 ng-app。 從 ng-app 往下,每個 Directive 建立的 Scope 都會一層一層連結下去,形成一個以 $rootScope 為根的鏈表,注意 Scope 還有同級的概念,形容更貼切我覺得應該是一棵樹。 我們大概看一下 Scope 都有哪些成員:   function Scope() {      this.$id = nextUid();      // 依次為: 階段、父 Scope、Watch 函數集、下一個同級 Scope、上一個同級 Scope、首個子級 Scope、最後一個子級 Scope      this.$$phase = this.$parent = this.$$watchers =                     this.$$nextSibling = this.$$prevSibling =                     this.$$childHead = this.$$childTail = null;          // 重寫 this 屬性以便支援原型鏈      this['this'] = this.$root =  this;      this.$$destroyed = false;      // 以當前 Scope 為內容相關的非同步求值隊列,也就是一堆 Angular 運算式      this.$$asyncQueue = [];      this.$$postDigestQueue = [];      this.$$listeners = {};      this.$$listenerCount = {};      this.$$isolateBindings = {};}Scope.$digest,這是 Angular 提供的從 Model 更新到 UI 的介面,你從哪個 Scope 調用,那它就會從這個 Scope 開始遍曆,通知模型更改給各個 watch 函數,來看看 $digest 的源碼: $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');     // 最後一個檢測到髒值的 watch 函數    lastDirtyWatch = null;     // 開始髒檢測,只要還有髒值或非同步隊列不為空白就會一直迴圈    do {      dirty = false;      // 當前遍曆到的 Scope      current = target;       // 處理非同步隊列中所有任務, 這個隊列由 scope.$evalAsync 方法輸入      while(asyncQueue.length) {        try {          asyncTask = asyncQueue.shift();          asyncTask.scope.$eval(asyncTask.expression);        } catch (e) {          clearPhase();          $exceptionHandler(e);        }        lastDirtyWatch = null;      }       traverseScopesLoop:      do {        // 取出當前 Scope 的所有 watch 函數        if ((watchers = current.$$watchers)) {          length = watchers.length;          while (length--) {            try {              watch = watchers[length];               if (watch) {                // 1.取 watch 函數的運算新值,直接與 watch 函數最後一次值比較                // 2.如果比較失敗則嘗試調用 watch 函數的 equal 函數,如果沒有 equal 函數則直接比較新舊值是否都是 number                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, null) : value;                  // 調用 watch 函數的變更通知函數, 也就是說各個 directive 從這裡更新 UI                  watch.fn(value, ((last === initWatchVal) ? value : last), current);                   // 當 digest 調用次數大於 5 的時候(預設10),記錄下來以便開發人員分析。                  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         // 沒有子級 Scope,也沒有同級 Scope        if (!(next = (current.$$childHead || (current !== target && current.$$nextSibling)))) {          // 又判斷一遍不知道為什麼,不過這個時候 next === undefined 了,也就退出當前 Scope 的 watch 遍曆了          while(current !== target && !(next = current.$$nextSibling)) {            current = current.$parent;          }        }      } while ((current = next));        // 當 TTL 用完,依舊有未處理的髒值和非同步隊列則拋出異常      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();     while(postDigestQueue.length) {      try {        postDigestQueue.shift()();      } catch (e) {        $exceptionHandler(e);      }    }  }雖然看起來很長,但是很容易理解,預設從 $rootScope 開始遍曆,對每個 watch 函數求值比較,出現新值則調用通知函數,由通知函數更新 UI,我們來看看 ng-model 是怎麼註冊通知函數的: $scope.$watch(function ngModelWatch() {    var value = ngModelGet($scope);     // 如果 ng-model 目前記錄的 modelValue 不等於 Scope 的最新值    if (ctrl.$modelValue !== value) {       var formatters = ctrl.$formatters,          idx = formatters.length;       // 使用格式化器格式新值,比如 number,email 之類      ctrl.$modelValue = value;      while(idx--) {        value = formatters[idx](value);      }       // 將新值更新到 UI      if (ctrl.$viewValue !== value) {        ctrl.$viewValue = value;        ctrl.$render();      }    }     return value;});那麼 UI 更改如何更新到 Model 呢 很簡單,靠 Directive 編譯時間綁定的事件,比如 ng-model 綁定到一個輸入框的時候事件代碼如下: var ngEventDirectives = {};forEach(  'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),function(name) {      var directiveName = directiveNormalize('ng-' + name);      ngEventDirectives[directiveName] = ['$parse', function($parse) {     return {       compile: function($element, attr) {         var fn = $parse(attr[directiveName]);         return function(scope, element, attr) {// 觸發以上指定的事件,就將元素的 scope 和 event 對象一起發送給 direcive           element.on(lowercase(name), function(event) {             scope.$apply(function() {               fn(scope, {$event:event});             });           });         };       }         };    }];  });Directive 接收到輸入事件後根據需要再去 Update Model 就好啦。 相信經過以上研究應該對 Angular 的綁定機制相當瞭解了吧,現在可別跟人家說起髒檢測就覺得是一個 while(true) 一直在求值效率好低什麼的,跟你平時用事件沒啥兩樣,多了幾次迴圈而已。 最後注意一點就是平時你通常不需要手動調用 scope.$digest,特別是當你的代碼在一個 $digest 中被回調的時候,因為已經進入了 digest 階段所以你再調用則會拋出異常。我們只在沒有 Scope 內容相關的代碼裡邊需要調用 digest,因為此時你對 UI 或 Model 的更改 Angular 並不知情。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.