為什麼你的Angular雙向數據綁定會失效?

為什麼你的Angular雙向數據綁定會失效?

來自專欄猿論4 人贊了文章

Angular雙向數據綁定原理探究。

文章源碼引用較多,覺得難以理解可以直接跳到末尾總結處。

接觸過Angular的人一定會對其「雙向數據綁定」的特性印象深刻,而使用過的人更會對莫名其妙出現的雙向數據綁定失效的「坑」所困擾。例如下面一段代碼:

<!--ctrl控制器下引入com指令--><body ng-app="app"> <div ng-controller="ctrl"> <input id="ipt" type="text" ng-model="value"> <button com>increase</button> <span id="span" ng-bind="value"></span> </div></body>var app = angular.module("app", [])app.directive("com", function() { return function (scope, element) { element.on("click", function() { //修改scope.value模型的值,觀察視圖變化 scope.value="yalishizhude" //疑問1:執行結果怎麼是 "" ? console.log(document.getElementById(span).textContent) }); };});app.controller("ctrl", function($scope) { var e = angular.element(document.querySelector(#ipt)) setTimeout(function() { //修改視圖元素的值,觀察$scope.value模型的值變化 e.val(100) //疑問2:執行結果是 undefined ? console.log($scope.value) }, 1000)});

源碼地址:jsbin.com/xogosim/edit?

如果上面代碼中的兩個問題你都知道答案,那麼你可以跳過下面的內容,如果並不完全清楚,那麼我們接著往下說~

雙向數據綁定,指的是視圖和模型之間的映射關係。雙向即 視圖 ==> 模型模型 ==> 視圖 兩個方向。

我們以Angular1.3為例,探究一下這個問題。

視圖 ==> 模型

拋開Angular不說,如果我們要實現視圖修改時觸發模型的修改,很簡單,事件(鍵盤事件、滑鼠事件、UI事件)監聽就能實現。而Angular會不會也是這麼實現的?

最常用的場景便是表單元素的數據綁定,當元素的值發生變化時我們要通知模型層(比如校驗、聯動),例如用於實現這一功能的 ngModel 指令。

但是我們如果直接找到ngModel的源碼,並沒有找到直接的事件綁定,依賴ngModelOptions指令倒是有一段代碼綁定了事件

//第23769行 if (modelCtrl.$options && modelCtrl.$options.updateOn) { element.on(modelCtrl.$options.updateOn, function(ev) { modelCtrl.$$debounceViewValueCommit(ev && ev.type); });}

可是平常沒使用ngModelOptions的時候也能同步元素的修改,難道是一開始就想錯了?

回憶一下Angular定義指令的時候,不光有像ngModel這樣通過屬性定義,也有直接定義成元素的,例如form就是一個指令。而最常用最簡單的就是把ngModel用在input元素上,不,應該是input指令。

於是找到input指令的代碼

//20436行var inputDirective = [$browser, $sniffer, $filter, $parse, function($browser, $sniffer, $filter, $parse) { return { restrict: E, require: [?ngModel], link: { pre: function(scope, element, attr, ctrls) { if (ctrls[0]) { (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer, $browser, $filter, $parse); } } } };}];

發現只要nhgModel指令存在的時候,它就會根據type屬性執行一段函數。

我們找到inputType.text這個函數之後,層層追尋...

//19928行 if ($sniffer.hasEvent(input)) { element.on(input, listener); } else { var timeout; var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { timeout = null; if (!input || input.value !== origValue) { listener(ev); } }); } }; element.on(keydown, function(event) { var key = event.keyCode; // ignore // command modifiers arrows if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; deferListener(event, this, this.value); }); // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it if ($sniffer.hasEvent(paste)) { element.on(paste cut, deferListener); } } // if user paste into input using mouse on older browser // or form autocomplete on newer browser, we need "change" event to catch it element.on(change, listener);

終於找到了它在綁定事件的證據,而且還很智能,根據瀏覽器對事件的支持情況來進行綁定。

發現綁定的事件都執行了一個函數:$setViewValue。繼續查找,發現調用ngModelSet函數來修改模型。

模型 ==> 視圖

我們再次拋開Angular,回到原生實現,如果我們想要修改視圖也比較簡單,獲取dom元素並修改對應的屬性。

再找一個在Angular中將模型值同步到dom上的指令ngBind

//20583行var ngBindDirective = [$compile, function($compile) { return { restrict: AC, compile: function ngBindCompile(templateElement) { $compile.$$addBindingClass(templateElement); return function ngBindLink(scope, element, attr) { $compile.$$addBindingInfo(element, attr.ngBind); element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { element.textContent = value === undefined ? : value; }); }; } };}];

發現其在scope.$watch回調函數中來修改dom元素的文本內容。那我們可以大膽地推測,應該是在修改了對應的$scope屬性值之後,觸發了scope.$watch調用了ngBindWatchAction回調函數才導致頁面元素文本變化的。

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

從源碼中可以看到,當我們在調用$watch監控變數的時候,其實是創建了一個watcher對象,並將其放入$scope.$$watchers數組中。

那麼誰會用到這個數組,並且其中的回調函數呢?

這個代碼有點難找,直到找到一個叫做$digest的函數定義。

//14394行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, null) : value; watch.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.$$childHead || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } }} while ((current = next));

簡單概括一下這段代碼,遍歷$scope.$$watchers,判斷如果需要檢測的表達式的值(可以理解為$scope的屬性)發生了修改,那麼執行對應回調函數(比如ngBindg中的ngBindWatchAction)。

修改$scope對應的屬性,並調用$scope.$digest。完成這兩個條件即可同步模型數據到視圖,修改dom元素。換句話說,這兩個條件缺一不可。而調用$scope.digest這一過程,我們一般叫做臟值檢測

有人可能會說我調用$scope.$apply也可以啊~

理論上來說,用$scope.$digest完成的手動試圖同步都可以用$scope.$apply,但是他們之間還是有區別。

//14666行$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是對$rootScope及子作用域做臟值檢測,意味著性能消耗更大。支持回掉函數算是一個好處。

總結

視圖 ==事件綁定==> 模型

模型 <==臟值檢測== 模型

作者:亞里士朱德

鏈接:imooc.com/article/18613

來源:慕課網

本文原創發佈於慕課網 ,轉載請註明出處,謝謝合作


推薦閱讀:

【重磅】認證作者招募 | 打造個人品牌 so easy !

有獎徵文003期|程序員進階路上,哪本書你認為很不錯,對你幫助很大?

如何基於區塊鏈技術開發應用

想要做到高並發和高性能,請先真正的理解它們,以及跟CPU,內存,分散式又有什麼關係呢

ASReader:一個經典的機器閱讀理解深度學習模型

推薦閱讀:

5min-Angular-03-Angular DOM Manipulation
angularjs項目需要從一個頁面跳轉到另一個頁面,同時需要傳遞一個參數。請問大神該通過什麼實現?
angularJs到底是幹什麼用的?
如何看2015年1月Peter-Paul Koch對Angular的看法?
放棄後端轉前端是否是個明智的選擇?

TAG:jQuery | AngularJS | React |