寫在開頭
成都創(chuàng)新互聯(lián)公司專注于網(wǎng)站建設(shè)|成都網(wǎng)站改版|優(yōu)化|托管以及網(wǎng)絡(luò)推廣,積累了大量的網(wǎng)站設(shè)計(jì)與制作經(jīng)驗(yàn),為許多企業(yè)提供了網(wǎng)站定制設(shè)計(jì)服務(wù),案例作品覆蓋活動(dòng)板房等行業(yè)。能根據(jù)企業(yè)所處的行業(yè)與銷售的產(chǎn)品,結(jié)合品牌形象的塑造,量身開發(fā)品質(zhì)網(wǎng)站。
關(guān)于Angular臟檢查,之前沒有仔細(xì)學(xué)習(xí),只是旁聽道說,Angular 會(huì)定時(shí)的進(jìn)行周期性數(shù)據(jù)檢查,將前臺(tái)和后臺(tái)數(shù)據(jù)進(jìn)行比較,所以非常損耗性能。
這是大錯(cuò)而特錯(cuò)的。我甚至在新浪前端面試的時(shí)候胡說一通,現(xiàn)在想來真是羞愧難當(dāng)! 沒有深入了解就信口開河實(shí)在難堪大任。
最后被拒也是理所當(dāng)然。
誤區(qū)糾正
首先糾正誤區(qū),Angular并不是周期性觸發(fā)藏檢查。
只有當(dāng)UI事件,ajax請(qǐng)求或者 timeout 延遲事件,才會(huì)觸發(fā)臟檢查。
為什么叫臟檢查? 對(duì)臟數(shù)據(jù)的檢查就是臟檢查,比較UI和后臺(tái)的數(shù)據(jù)是否一致!
下面解釋:
$watch 對(duì)象。
Angular 每一個(gè)綁定到UI的數(shù)據(jù),就會(huì)有一個(gè) $watch 對(duì)象。
這個(gè)對(duì)象包含三個(gè)參數(shù)
watch = { name:'', //當(dāng)前的watch 對(duì)象 觀測(cè)的數(shù)據(jù)名 getNewValue:function($scope){ //得到新值 ... return newValue; }, listener:function(newValue,oldValue){ // 當(dāng)數(shù)據(jù)發(fā)生改變時(shí)需要執(zhí)行的操作 ... } }
getNewValue() 可以得到當(dāng)前$scope 上的最新值,listener 函數(shù)得到新值和舊值并進(jìn)行一些操作。
而常常我們?cè)谑褂肁ngular的時(shí)候,listener 一般都為空,只有當(dāng)我們需要監(jiān)測(cè)更改事件的時(shí)候,才會(huì)顯示地添加監(jiān)聽。
每當(dāng)我們將數(shù)據(jù)綁定到 UI 上,angular 就會(huì)向你的 watchList 上插入一個(gè) $watch。
比如:
<span>{{user}}</span> <span>{{password}}</span>
這就會(huì)插入兩個(gè)$watch 對(duì)象。
之后,開始臟檢查。
好了,我們先把臟檢查放一放,來看它之前的東西
雙向數(shù)據(jù)綁定 ! 只有先理解了Angular的雙向數(shù)據(jù)綁定,才能透徹理解臟檢查 。
雙向數(shù)據(jù)綁定
Angular實(shí)現(xiàn)了雙向數(shù)據(jù)綁定。無非就是界面的操作能實(shí)事反應(yīng)到數(shù)據(jù),數(shù)據(jù)的更改也能在界面呈現(xiàn)。
界面到數(shù)據(jù)的更改,是由 UI 事件,ajax請(qǐng)求,或者timeout 等回調(diào)操作,而數(shù)據(jù)到界面的呈現(xiàn)則是由臟檢查來做.
這也是我開始糾正的誤區(qū)
只有當(dāng)觸發(fā)UI事件,ajax請(qǐng)求或者 timeout 延遲,才會(huì)觸發(fā)臟檢查。
看下面的例子
<div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</button> </div>
function CounterCtrl($scope) { $scope.counter = 1; }
毫無疑問,我每點(diǎn)擊一次button,counter就會(huì)+1,因?yàn)辄c(diǎn)擊事件,將couter+1,而后觸發(fā)了臟檢查,又將新值2 返回給了界面.
這就是一個(gè)簡單的雙向數(shù)據(jù)綁定的流程.
但是就只有這么簡單嗎??
看下面的代碼
'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>
點(diǎn)擊后,毫無反應(yīng).
試試在 console.log(scope.data) 后面添加 scope.$digest(); 試試?
很明顯,數(shù)據(jù)增加了。如果使用$apply () 呢? 當(dāng)然可以(后面會(huì)接受 $apply 和 $digest 的區(qū)別)
為什們呢?
假設(shè)沒有AngularJS,要讓我們自己實(shí)現(xiàn)這個(gè)類似的功能,該怎么做呢?
<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(); }
測(cè)試一下:
可以看到我們沒有直接使用DOM的onclick方法,而是搞了一個(gè)ng-click,然后在bind里面把這個(gè)ng-click對(duì)應(yīng)的函數(shù)拿出來,綁定到onclick的事件處理函數(shù)中。為什么要這樣呢?因?yàn)閿?shù)據(jù)雖然變更了,但是還沒有往界面上填充,我們需要在此做一些附加操作。
另外,由于雙向綁定機(jī)制,在DOM操作中,雖然更新了數(shù)據(jù)的值,但是并沒有立即反映到界面上,而是通過 apply() 來反映到界面上,從而完成職責(zé)的分離,可以認(rèn)為是單一職責(zé)模式了。
在真正的Angular中,ng-click 封裝了click,然后調(diào)用一次 apply 函數(shù),把數(shù)據(jù)呈現(xiàn)到界面上
在Angular 的apply函數(shù)中,這里先進(jìn)行臟檢測(cè),看 oldValue 和 newVlue 是否相等,如果不相等,那么講newValue 反饋到界面上,通過如果通過 $watch 注冊(cè)了 listener事件,那么就會(huì)調(diào)用該事件。
臟檢查的優(yōu)缺點(diǎn)
經(jīng)過我們上面的分析,可以總結(jié):
然而就有了接下來的討論?
不斷觸發(fā)臟檢查是不是一種好的方式?
有很多人認(rèn)為,這樣對(duì)性能的損耗很大,不如 setter 和 getter 的觀察者模式。 但是我們看下面這個(gè)例子
<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++; } } }
在臟檢測(cè)的機(jī)制下,這個(gè)過程毫無壓力,會(huì)等待到 循環(huán)執(zhí)行結(jié)束,然后一次更新 checkedItemsNumber,應(yīng)用到界面上。 但是在基于setter的機(jī)制就慘了,每變化一次checkedItemsNumber就需要更新一次,這樣性能就會(huì)極低。
所以說,兩種不同的監(jiān)控方式,各有其優(yōu)缺點(diǎn),最好的辦法是了解各自使用方式的差異,考慮出它們性能的差異所在,在不同的業(yè)務(wù)場(chǎng)景中,避開最容易造成性能瓶頸的用法。
好了,現(xiàn)在已經(jīng)了解了雙向數(shù)據(jù)綁定了 臟檢查的觸發(fā)機(jī)制,那么,臟檢查內(nèi)部又是怎么實(shí)現(xiàn)的呢?
臟檢查的內(nèi)部實(shí)現(xiàn)
首先,構(gòu)造$scope 對(duì)象,
function $scope = function(){}
現(xiàn)在,我們回到開頭 $watch。
我們說,每一個(gè)綁定到UI上的數(shù)據(jù)都有擁有一個(gè)對(duì)應(yīng)的$watch 對(duì)象,這個(gè)對(duì)象會(huì)被push到watchList中。
它擁有兩個(gè)函數(shù)作為屬性
還有一個(gè)字符串屬性
name: 當(dāng)前watch作用的變量名
function $scope(){ this. $$watchList = []; }
在Angular框架中,雙美元符前綴$$表示這個(gè)變量被當(dāng)作私有的來考慮,不應(yīng)當(dāng)在外部代碼中調(diào)用。
現(xiàn)在我們可以定義$watch方法了。它接受兩個(gè)函數(shù)作參數(shù),把它們存儲(chǔ)在$$watchers數(shù)組中。我們需要在每個(gè)Scope實(shí)例上存儲(chǔ)這些函數(shù),所以要把它放在Scope的原型上:
$scope.prototype.$watch = function(name,getNewValue,listener){ var watch = { name:name, getNewValue : getNewValue, listener : listener }; this.$$watchList.push(watch); }
另外一面就是$digest函數(shù)。它執(zhí)行了所有在作用域上注冊(cè)過的監(jiān)聽器。我們來實(shí)現(xiàn)一個(gè)它的簡化版,遍歷所有監(jiān)聽器,調(diào)用它們的監(jiān)聽函數(shù):
$scope.prototype.$digest = function(){ var list = this.$$watchList; for(var i = 0,l = list.length;i<l;i++){ list[i].listener(); } }
現(xiàn)在,我們就可以添加監(jiān)聽器并且運(yùn)行臟檢查了。
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();
代碼會(huì)托管到github,測(cè)試文件路徑跟命令中路徑一致
OK,兩個(gè)監(jiān)聽均已經(jīng)觸發(fā)。
這些本身沒什么大用,我們要的是能檢測(cè)由getNewValue返回指定的值是否確實(shí)變更了,然后調(diào)用監(jiān)聽函數(shù)。
那么,我們需要在getNewValue() 上每次都得到數(shù)據(jù)上最新的值,所以需要得到當(dāng)前的scope對(duì)象
getNewValue = function(scope){ return scope[this.name]; }
是監(jiān)控函數(shù)的一般形式:從作用域獲取一些值,然后返回。
$digest函數(shù)的作用是調(diào)用這個(gè)監(jiān)控函數(shù),并且比較它返回的值和上一次返回值的差異。如果不相同,監(jiān)聽器就是臟的,它的監(jiān)聽函數(shù)就應(yīng)當(dāng)被調(diào)用。
想要這么做,$digest需要記住每個(gè)監(jiān)控函數(shù)上次返回的值。既然我們現(xiàn)在已經(jīng)為每個(gè)監(jiān)聽器創(chuàng)建過一個(gè)對(duì)象,只要把上一次的值存在這上面就行了。下面是檢測(cè)每個(gè)監(jiān)控函數(shù)值變更的$digest新實(shí)現(xiàn):
$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); // 在第一次渲染界面,進(jìn)行一個(gè)數(shù)據(jù)呈現(xiàn). var oldValue = watch.last; if(newValue!=oldValue){ watch.listener(newValue,oldValue); } watch.last = newValue; } }
對(duì)于每一個(gè)watch,我們使用 getNewValue() 并且把scope實(shí)例 傳遞進(jìn)去,得到數(shù)據(jù)最新值 。然后和上一次值進(jìn)行比較,如果不同,那就調(diào)用 getListener,同時(shí)把新值和舊值一并傳遞進(jìn)去。 最終,我們把last 屬性設(shè)置為新返回的值,也就是最新值。
這個(gè)$digest 再一次調(diào)用,last 為undefined,所以一定會(huì)進(jìn)行一次數(shù)據(jù)呈現(xiàn)。
好了,我們看看這個(gè)監(jiān)控函數(shù)如何運(yùn)行的
var scope = new $scope(); scope.hello = 10; scope.$watch('hello', function(scope) { // 注意,要理解這里的this ,這個(gè)函數(shù)實(shí)際是 var newValue = watch.getNewValue(this); 這樣調(diào)用,那么 this 就指的是當(dāng)前監(jiān)聽器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();
運(yùn)行結(jié)果
我們已經(jīng)實(shí)現(xiàn)了Angular作用域的本質(zhì):添加監(jiān)聽器,在digest里運(yùn)行它們。
也已經(jīng)可以看到幾個(gè)關(guān)于Angular作用域的重要性能特性:
有時(shí)候并不需要注冊(cè)那么多的Listener
在看我們上面的程序:
$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); // 在第一次渲染界面,進(jìn)行一個(gè)數(shù)據(jù)呈現(xiàn). var oldValue = watch.last; if(newValue!=oldValue){ watch.listener(newValue,oldValue); } watch.last = newValue; } }
我們這樣做,就要求每個(gè)監(jiān)聽器watch 都必須注冊(cè) listener,然而事實(shí)是:在Angular 應(yīng)用中,只有少數(shù)的監(jiān)聽器需要注冊(cè)listener。
更改 $scope.prototype.$wacth,在這里放置一個(gè)空的函數(shù)。
$scope.prototype.$watch = function(name,getNewValue,listener){ var watch = { name:name, getNewValue : getNewValue, listener : listener || function(){} }; this.$$watchList.push(watch); }
貌似這樣已經(jīng)初步理解了臟檢查原理,但是一個(gè)重要的問題我們忽視了。
先后注冊(cè)了兩個(gè)監(jiān)聽器,第二個(gè)監(jiān)聽器的listener 改變了 第一個(gè)監(jiān)聽器對(duì)應(yīng)數(shù)據(jù)的值,那么這么做會(huì)檢測(cè)的到嗎?
看下面的例子
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,已經(jīng)發(fā)生改變,但是界面上的值卻沒有改變。
現(xiàn)在來修復(fù)這個(gè)問題。
當(dāng)數(shù)據(jù)臟的時(shí)候持續(xù)Digest
我們需要改變一下digest,讓它持續(xù)遍歷所有監(jiān)聽器,直到監(jiān)控的值停止變更。
首先,我們把現(xiàn)在的$digest函數(shù)改名為$$digestOnce,它把所有的監(jiān)聽器運(yùn)行一次,返回一個(gè)布爾值,表示是否還有變更了。
$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); // 因?yàn)閘istener操作,已經(jīng)檢查過的數(shù)據(jù)可能變臟 dirty = true; } watch.last = newValue; return dirty; } };
然后,我們重新定義$digest,它作為一個(gè)“外層循環(huán)”來運(yùn)行,當(dāng)有變更發(fā)生的時(shí)候,調(diào)用$$digestOnce:
$scope.prototype.$digest = function() { var dirty = true; while(dirty) { dirty = this.$$digestOnce(); } };
$digest現(xiàn)在至少運(yùn)行每個(gè)監(jiān)聽器一次了。如果第一次運(yùn)行完,有監(jiān)控值發(fā)生變更了,標(biāo)記為dirty,所有監(jiān)聽器再運(yùn)行第二次。這會(huì)一直運(yùn)行,直到所有監(jiān)控的值都不再變化,整個(gè)局面穩(wěn)定下來了。
在Angular作用域里并不是真的有個(gè)函數(shù)叫做$$digestOnce,相反,digest循環(huán)都是包含在$digest里的。我們的目標(biāo)更多是清晰度而不是性能,所以把內(nèi)層循環(huán)封裝成了一個(gè)函數(shù)。
測(cè)試一下
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);
可以看到,現(xiàn)在界面上的數(shù)據(jù)已經(jīng)全部為最新
我們現(xiàn)在可以對(duì)Angular的監(jiān)聽器有另外一個(gè)重要認(rèn)識(shí):它們可能在單次digest里面被執(zhí)行多次。這也就是為什么人們經(jīng)常說,監(jiān)聽器應(yīng)當(dāng)是冪等的:一個(gè)監(jiān)聽器應(yīng)當(dāng)沒有邊界效應(yīng),或者邊界效應(yīng)只應(yīng)當(dāng)發(fā)生有限次。比如說,假設(shè)一個(gè)監(jiān)控函數(shù)觸發(fā)了一個(gè)Ajax請(qǐng)求,無法確定你的應(yīng)用程序發(fā)了多少個(gè)請(qǐng)求。
如果兩個(gè)監(jiān)聽器循環(huán)改變呢?像現(xiàn)在這樣:
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 ++; })
那么,臟檢查就不會(huì)停下來,一直循環(huán)下去。如何解決呢?
更穩(wěn)定的 $digest
我們要做的事情是,把digest的運(yùn)行控制在一個(gè)可接受的迭代數(shù)量內(nèi)。如果這么多次之后,作用域還在變更,就勇敢放手,宣布它永遠(yuǎn)不會(huì)穩(wěn)定。在這個(gè)點(diǎn)上,我們會(huì)拋出一個(gè)異常,因?yàn)椴还茏饔糜虻臓顟B(tài)變成怎樣,它都不太可能是用戶想要的結(jié)果。
迭代的最大值稱為TTL(short for Time To Live)。這個(gè)值默認(rèn)是10,可能有點(diǎn)小(我們剛運(yùn)行了這個(gè)digest 100,000次?。?,但是記住這是一個(gè)性能敏感的地方,因?yàn)閐igest經(jīng)常被執(zhí)行,而且每個(gè)digest運(yùn)行了所有的監(jiān)聽器。用戶也不太可能創(chuàng)建10個(gè)以上鏈狀的監(jiān)聽器。
我們繼續(xù),給外層digest循環(huán)添加一個(gè)循環(huán)計(jì)數(shù)器。如果達(dá)到了TTL,就拋出異常:
$scope.prototype.$digest = function() { var dirty = true; var checkTimes = 0; while(dirty) { dirty = this.$$digestOnce(); checkTimes++; if(checkTimes>10 &&dirty){ throw new Error("檢測(cè)超過10次"); console.log("123"); } }; };
測(cè)試一下
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();
好了,關(guān)于 Angular 臟檢查和 雙向數(shù)據(jù)綁定原理就介紹到這里,雖然離真正的Angular 還差很多,但是也能基本解釋原理了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。
新聞名稱:AngularJS的臟檢查深入分析
文章鏈接:http://aaarwkj.com/article46/igjehg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供靜態(tài)網(wǎng)站、商城網(wǎng)站、關(guān)鍵詞優(yōu)化、網(wǎng)站建設(shè)、建站公司、做網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)