AngularJs 的元素與模型雙向綁定依賴於循環檢測它們之間的值,這種作法叫作髒檢測,這幾天研究了一下其源碼,將 Angular 的實現分享一下。express
首先看看如何將 Model 的變動更新到 UIapp
Angular 的 Model 是一個 Scope 的類型,每一個 Scope 都歸屬於一個 Directive 對象,好比 $rootScope 就歸屬於 ng-app。異步
從 ng-app 往下,每一個 Directive 建立的 Scope 都會一層一層連接下去,造成一個以 $rootScope 爲根的鏈表,注意 Scope 還有同級的概念,形容更貼切我以爲應該是一棵樹。async
咱們大概看一下 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 的源碼:oop
$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 並且都是 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; 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 是怎麼註冊通知函數的:post
$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 呢ui
很簡單,靠 Directive 編譯時綁定的事件,好比 ng-model 綁定到一個輸入框的時候事件代碼以下:this
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 並不知情。