關於Angular髒檢查,以前沒有仔細學習,只是旁聽道說,Angular 會定時的進行週期性數據檢查,將前臺和後臺數據進行比較,因此很是損耗性能。javascript
這是大錯而特錯的。我甚至在新浪前端面試的時候胡說一通,如今想來真是羞愧難當! 沒有深刻了解就信口開河實在難堪大任。html
最後被拒也是理所固然。前端
在剖析以前,很是感謝坐鎮蘇寧的徐飛,如今已經不在蘇寧了,我也是在他翻譯的文章(Build Your own AngularJS)和博客才略懂一二。
徐飛關於知乎問題國內前端團隊分佈和前景是怎樣的?的回答也是特別有意思。java
首先糾正誤區,Angular並非週期性觸發藏檢查。
只有當UI事件,ajax請求或者 timeout 延遲事件,纔會觸發髒檢查。
爲何叫髒檢查? 對髒數據的檢查就是髒檢查,比較UI和後臺的數據是否一致!
下面解釋:git
Angular 每個綁定到UI的數據,就會有一個 $watch 對象。
這個對象包含三個參數github
watch = { name:'', //當前的watch 對象 觀測的數據名 getNewValue:function($scope){ //獲得新值 ... return newValue; }, listener:function(newValue,oldValue){ // 當數據發生改變時須要執行的操做 ... } }
getNewValue() 能夠獲得當前$scope 上的最新值,listener 函數獲得新值和舊值並進行一些操做。面試
而經常咱們在使用Angular的時候,listener 通常都爲空,只有當咱們須要監測更改事件的時候,纔會顯示地添加監聽。ajax
每當咱們將數據綁定到 UI 上,angular 就會向你的 watchList 上插入一個 $watch。
好比:數組
<span>{{user}}</span> <span>{{password}}</span>
這就會插入兩個$watch 對象。
以後,開始髒檢查。
好了,咱們先把髒檢查放一放,來看它以前的東西
??
雙向數據綁定 ! 只有先理解了Angular的雙向數據綁定,才能透徹理解髒檢查 。app
Angular實現了雙向數據綁定。無非就是界面的操做能實事反應到數據,數據的更改也能在界面呈現。
界面到數據的更改,是由 UI 事件,ajax請求,或者timeout 等回調操做,而數據到界面的呈現則是由髒檢查來作.
這也是我開始糾正的誤區
只有當觸發UI事件,ajax請求或者 timeout 延遲,纔會觸發髒檢查。
看下面的例子
<div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</button> </div>
function CounterCtrl($scope) { $scope.counter = 1; }
毫無疑問,我每點擊一次button,counter就會+1,由於點擊事件,將couter+1,然後觸發了髒檢查,又將新值2 返回給了界面.
這就是一個簡單的雙向數據綁定的流程.
可是就只有這麼簡單嗎??
看下面的代碼
'use strict'; var app = angular.module('app', []); app.directive('myclick', function() { return function(scope, element, attr) { element.on('click', function() { scope.data++; console.log(scope.data) }) } }) app.controller('appController', function($scope) { $scope.data = 0; });
<div ng-app="app"> <div ng-controller="appController"> <span>{{data}}</span> <button myclick>click</button> </div> </div>
點擊後,毫無反應.
???
試試在 console.log(scope.data) 後面添加 scope.$digest(); 試試?
很明顯,數據增長了。若是使用$apply () 呢? 固然能夠(後面會接受 $apply 和 $digest 的區別)
爲什們呢?
假設沒有AngularJS,要讓咱們本身實現這個相似的功能,該怎麼作呢?
<body> <button ng-click="increase">increase</button> <button ng-click="decrease">decrease</button> <span ng-bind="data"></span> <script src="app.js"></script> </body>
window.onload = function() { 'use strict'; var scope = { increase: function() { this.data++; }, decrease: function decrease() { this.data--; }, data: 0 } function bind() { var list = document.querySelectorAll('[ng-click]'); for (var i = 0, l = list.length; i < l; i++) { list[i].onclick = (function(index) { return function() { var func = this.getAttribute('ng-click'); scope[func](scope); apply(); } })(i); } } // apply function apply() { var list = document.querySelectorAll('[ng-bind]'); for (var i = 0, l = list.length; i < l; i++) { var bindData = list[i].getAttribute('ng-bind'); list[i].innerHTML = scope[bindData]; } } bind(); apply(); }
測試一下:
能夠看到咱們沒有直接使用DOM的onclick方法,而是搞了一個ng-click,而後在bind裏面把這個ng-click對應的函數拿出來,綁定到onclick的事件處理函數中。爲何要這樣呢?由於數據雖然變動了,可是尚未往界面上填充,咱們須要在此作一些附加操做。
另外,因爲雙向綁定機制,在DOM操做中,雖然更新了數據的值,可是並無當即反映到界面上,而是經過 apply() 來反映到界面上,從而完成職責的分離,能夠認爲是單一職責模式了。
在真正的Angular中,ng-click 封裝了click,而後調用一次 apply 函數,把數據呈現到界面上
在Angular 的apply函數中,這裏先進行髒檢測,看 oldValue 和 newVlue 是否相等,若是不相等,那麼講newValue 反饋到界面上,經過若是經過 $watch 註冊了 listener事件,那麼就會調用該事件。
通過咱們上面的分析,能夠總結:
然而就有了接下來的討論?
不斷觸發髒檢查是否是一種好的方式?
有不少人認爲,這樣對性能的損耗很大,不如 setter 和 getter 的觀察者模式。 可是咱們看下面這個例子
<span>{{checkedItemsNumber}}</span>
function Ctrl($scope){ var list = []; $scope.checkedItemsNumber = 0; for(var i = 0;i<1000;i++){ list.push(false); } $scope.toggleChecked = function(flag){ for(var i = 0,l= list.length;i++){ list[i] = flag; $scope.checkedItemsNumber++; } } }
在髒檢測的機制下,這個過程毫無壓力,會等待到 循環執行結束,而後一次更新 checkedItemsNumber,應用到界面上。 可是在基於setter的機制就慘了,每變化一次checkedItemsNumber就須要更新一次,這樣性能就會極低。
因此說,兩種不一樣的監控方式,各有其優缺點,最好的辦法是瞭解各自使用方式的差別,考慮出它們性能的差別所在,在不一樣的業務場景中,避開最容易形成性能瓶頸的用法。
好了,如今已經瞭解了雙向數據綁定了 髒檢查的觸發機制,那麼,髒檢查內部又是怎麼實現的呢?
首先,構造$scope 對象,
function $scope = function(){}
如今,咱們回到開頭 $watch。
咱們說,每個綁定到UI上的數據都有擁有一個對應的$watch 對象,這個對象會被push到watchList中。
它擁有兩個函數做爲屬性
function $scope(){ this. $$watchList = []; }
在Angular框架中,雙美圓符前綴$$表示這個變量被看成私有的來考慮,不該當在外部代碼中調用。
如今咱們能夠定義$watch方法了。它接受兩個函數做參數,把它們存儲在$$watchers數組中。咱們須要在每一個Scope實例上存儲這些函數,因此要把它放在Scope的原型上:
$scope.prototype.$watch = function(name,getNewValue,listener){ var watch = { name:name, getNewValue : getNewValue, listener : listener }; this.$$watchList.push(watch); }
另一面就是$digest函數。它執行了全部在做用域上註冊過的監聽器。咱們來實現一個它的簡化版,遍歷全部監聽器,調用它們的監聽函數:
$scope.prototype.$digest = function(){ var list = this.$$watchList; for(var i = 0,l = list.length;i<l;i++){ list[i].listener(); } }
如今,咱們就能夠添加監聽器而且運行髒檢查了。
var scope = new Scope(); scope.$watch(function() { console.log("hey i have got newValue") }, function() { console.log("i am the listener"); }) scope.$watch(function() { console.log("hey i have got newValue 2") }, function() { console.log("i am the listener2"); }) scope.$disget();
代碼會託管到github,測試文件路徑跟命令中路徑一致
OK,兩個監聽均已經觸發。
這些自己沒什麼大用,咱們要的是能檢測由getNewValue返回指定的值是否確實變動了,而後調用監聽函數。
那麼,咱們須要在getNewValue() 上每次都獲得數據上最新的值,因此須要獲得當前的scope對象
getNewValue = function(scope){ return scope[this.name]; }
是監控函數的通常形式:從做用域獲取一些值,而後返回。
$digest函數的做用是調用這個監控函數,而且比較它返回的值和上一次返回值的差別。若是不相同,監聽器就是髒的,它的監聽函數就應當被調用。
想要這麼作,$digest須要記住每一個監控函數上次返回的值。既然咱們如今已經爲每一個監聽器建立過一個對象,只要把上一次的值存在這上面就好了。下面是檢測每一個監控函數值變動的$digest新實現:
$scope.prototype.$digest = function(){ var list = this.$$watchList; for(var i = 0,l= list.length;i++){ var watch = list[i]; var newValue = watch.getNewValue(this); // 在第一次渲染界面,進行一個數據呈現. var oldValue = watch.last; if(newValue!=oldValue){ watch.listener(newValue,oldValue); } watch.last = newValue; } }
對於每個watch,咱們使用 getNewValue() 而且把scope實例 傳遞進去,獲得數據最新值 。而後和上一次值進行比較,若是不一樣,那就調用 getListener,同時把新值和舊值一併傳遞進去。 最終,咱們把last 屬性設置爲新返回的值,也就是最新值。
這個$digest 再一次調用,last 爲undefined,因此必定會進行一次數據呈現。
好了,咱們看看這個監控函數如何運行的
var scope = new $scope(); scope.hello = 10; scope.$watch('hello', function(scope) { // 注意,要理解這裏的this ,這個函數實際是 var newValue = watch.getNewValue(this); 這樣調用,那麼 this 就指的是當前監聽器watch,因此能夠獲得name return scope[this.name] }, function(newValue, oldValue) { console.log('newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$digest(); scope.hello = 10; scope.$digest(); scope.hello = 20; scope.$digest();
運行結果
咱們已經實現了Angular做用域的本質:添加監聽器,在digest裏運行它們。
也已經能夠看到幾個關於Angular做用域的重要性能特性:
在看咱們上面的程序:
$scope.prototype.$digest = function(){ var list = this.$$watchList; for(var i = 0,l= list.length;i++){ var watch = list[i]; var newValue = watch.getNewValue(this); // 在第一次渲染界面,進行一個數據呈現. var oldValue = watch.last; if(newValue!=oldValue){ watch.listener(newValue,oldValue); } watch.last = newValue; } }
咱們這樣作,就要求每一個監聽器watch 都必須註冊 listener,然而事實是:在Angular 應用中,只有少數的監聽器須要註冊listener。
更改 $scope.prototype.$wacth,在這裏放置一個空的函數。
$scope.prototype.$watch = function(name,getNewValue,listener){ var watch = { name:name, getNewValue : getNewValue, listener : listener || function(){} }; this.$$watchList.push(watch); }
貌似這樣已經初步理解了髒檢查原理,可是一個重要的問題咱們忽視了。
前後註冊了兩個監聽器,第二個監聽器的listener 改變了 第一個監聽器對應數據的值,那麼這麼作會檢測的到嗎?
看下面的例子
var scope = new $scope(); scope.first = 10; scope.second = 1; scope.$watch('first', function(scope) { return scope[this.name] }, function(newValue, oldValue) { console.log('first: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$watch('second', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.first = 8; console.log('second: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$digest(); console.log(scope.first); console.log(scope.second);
能夠看到,值爲 8,1,已經發生改變,可是界面上的值卻沒有改變。
如今來修復這個問題。
咱們須要改變一下digest,讓它持續遍歷全部監聽器,直到監控的值中止變動。
首先,咱們把如今的$digest函數更名爲$$digestOnce,它把全部的監聽器運行一次,返回一個布爾值,表示是否還有變動了。
$scope.prototype.$$digestOnce = function() { var dirty; var list = this.$$watchList; for(var i = 0,l = list.length;i<l;i++ ){ var watch = list[i]; var newValue = watch.getNewValue(this.name); var oldValue = watch.last; if(newValue !==oldValue){ watch.listener(newValue,oldValue); // 由於listener操做,已經檢查過的數據可能變髒 dirty = true; } watch.last = newValue; return dirty; } };
而後,咱們從新定義$digest,它做爲一個「外層循環」來運行,當有變動發生的時候,調用$$digestOnce:
$scope.prototype.$digest = function() { var dirty = true; while(dirty) { dirty = this.$$digestOnce(); } };
$digest如今至少運行每一個監聽器一次了。若是第一次運行完,有監控值發生變動了,標記爲dirty,全部監聽器再運行第二次。這會一直運行,直到全部監控的值都再也不變化,整個局面穩定下來了。
在Angular做用域裏並非真的有個函數叫作$$digestOnce,相反,digest循環都是包含在$digest裏的。咱們的目標更可能是清晰度而不是性能,因此把內層循環封裝成了一個函數。
測試一下
var scope = new $scope(); scope.first = 10; scope.second = 1; scope.$watch('first', function(scope) { return scope[this.name] }, function(newValue, oldValue) { console.log('first: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$watch('second', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.first = 8; console.log('second: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$digest(); console.log(scope.first); console.log(scope.second);
能夠看到,如今界面上的數據已經所有爲最新
咱們如今能夠對Angular的監聽器有另一個重要認識:它們可能在單次digest裏面被執行屢次。這也就是爲何人們常常說,監聽器應當是冪等的:一個監聽器應當沒有邊界效應,或者邊界效應只應當發生有限次。好比說,假設一個監控函數觸發了一個Ajax請求,沒法肯定你的應用程序發了多少個請求。
若是兩個監聽器循環改變呢?像如今這樣:
var scope = new $scope(); scope.first = 10; scope.second = 1; scope.$watch('first', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.second ++; }) scope.$watch('second', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.first ++; })
那麼,髒檢查就不會停下來,一直循環下去。如何解決呢?
咱們要作的事情是,把digest的運行控制在一個可接受的迭代數量內。若是這麼屢次以後,做用域還在變動,就勇敢放手,宣佈它永遠不會穩定。在這個點上,咱們會拋出一個異常,由於無論做用域的狀態變成怎樣,它都不太多是用戶想要的結果。
迭代的最大值稱爲TTL(short for Time To Live)。這個值默認是10,可能有點小(咱們剛運行了這個digest 100,000次!),可是記住這是一個性能敏感的地方,由於digest常常被執行,並且每一個digest運行了全部的監聽器。用戶也不太可能建立10個以上鍊狀的監聽器。
咱們繼續,給外層digest循環添加一個循環計數器。若是達到了TTL,就拋出異常:
$scope.prototype.$digest = function() { var dirty = true; var checkTimes = 0; while(dirty) { dirty = this.$$digestOnce(); checkTimes++; if(checkTimes>10 &&dirty){ throw new Error("檢測超過10次"); console.log("123"); } }; };
測試一下
var scope = new $scope(); scope.first = 1; scope.second = 10; scope.$watch('first', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.second++; console.log('first: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$watch('second', function(scope) { return scope[this.name] }, function(newValue, oldValue) { scope.first++; console.log('second: newValue:' + newValue + '~~~~' + 'oldValue:' + oldValue); }) scope.$digest();
好了,關於 Angular 髒檢查和 雙向數據綁定原理就介紹到這裏,雖然離真正的Angular 還差不少,可是也能基本解釋原理了。
關於 這篇中全部的代碼,已經放到github上 https://github.com/apawn/HFLib/tree/master/HFLib/angular .
推薦一本原著 《Build Your Own AngularJS》,書中詳細介紹瞭如何構建一個AngularJS。估計翻譯版本會在年後出版,若是能夠讀完這本書,那麼 JS的能力將會上升一個等級。
關於司徒正美的 《Javascript框架設計》 也是前端深刻研究的必讀之書。
後面在閱讀的時候,我會把本身的閱讀經驗分享出來。
只是把這些搞明白以後,如今再也沒有去面試新浪應屆生的機會了 。
雖然不知道明年會在哪,但必定會進入一個優秀的前端團隊並努力展現更好的面貌的。
若是您有意,歡迎聯繫我,Email:mymeat@126.com
在這篇中,我有提到 VueJS 中 基於 setter 和 getter 的實現,我講會深刻學習並在下一篇介紹。