溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

AngularJS的臟檢查深入分析

發(fā)布時間:2020-08-24 04:19:57 來源:腳本之家 閱讀:207 作者:Pawn.風為裳 欄目:web開發(fā)

寫在開頭

關于Angular臟檢查,之前沒有仔細學習,只是旁聽道說,Angular 會定時的進行周期性數(shù)據(jù)檢查,將前臺和后臺數(shù)據(jù)進行比較,所以非常損耗性能。

這是大錯而特錯的。我甚至在新浪前端面試的時候胡說一通,現(xiàn)在想來真是羞愧難當! 沒有深入了解就信口開河實在難堪大任。

最后被拒也是理所當然。

誤區(qū)糾正

首先糾正誤區(qū),Angular并不是周期性觸發(fā)藏檢查。

只有當UI事件,ajax請求或者 timeout 延遲事件,才會觸發(fā)臟檢查。

為什么叫臟檢查? 對臟數(shù)據(jù)的檢查就是臟檢查,比較UI和后臺的數(shù)據(jù)是否一致!

下面解釋:

$watch 對象。

Angular 每一個綁定到UI的數(shù)據(jù),就會有一個 $watch 對象。

這個對象包含三個參數(shù)

watch = {
 name:'',  //當前的watch 對象 觀測的數(shù)據(jù)名
 getNewValue:function($scope){ //得到新值
  ...
  return newValue;
  },
 listener:function(newValue,oldValue){ // 當數(shù)據(jù)發(fā)生改變時需要執(zhí)行的操作
  ...
 }
}

getNewValue() 可以得到當前$scope 上的最新值,listener 函數(shù)得到新值和舊值并進行一些操作。

而常常我們在使用Angular的時候,listener 一般都為空,只有當我們需要監(jiān)測更改事件的時候,才會顯示地添加監(jiān)聽。

每當我們將數(shù)據(jù)綁定到 UI 上,angular 就會向你的 watchList 上插入一個 $watch。

比如:

<span>{{user}}</span>
<span>{{password}}</span>

這就會插入兩個$watch 對象。

之后,開始臟檢查。

好了,我們先把臟檢查放一放,來看它之前的東西

雙向數(shù)據(jù)綁定 ! 只有先理解了Angular的雙向數(shù)據(jù)綁定,才能透徹理解臟檢查 。

雙向數(shù)據(jù)綁定

Angular實現(xiàn)了雙向數(shù)據(jù)綁定。無非就是界面的操作能實事反應到數(shù)據(jù),數(shù)據(jù)的更改也能在界面呈現(xiàn)。

界面到數(shù)據(jù)的更改,是由 UI 事件,ajax請求,或者timeout 等回調操作,而數(shù)據(jù)到界面的呈現(xiàn)則是由臟檢查來做.

這也是我開始糾正的誤區(qū)

只有當觸發(fā)UI事件,ajax請求或者 timeout 延遲,才會觸發(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;
}

毫無疑問,我每點擊一次button,counter就會+1,因為點擊事件,將couter+1,而后觸發(fā)了臟檢查,又將新值2 返回給了界面.

這就是一個簡單的雙向數(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>

點擊后,毫無反應.

試試在 console.log(scope.data) 后面添加 scope.$digest(); 試試?

很明顯,數(shù)據(jù)增加了。如果使用$apply () 呢? 當然可以(后面會接受 $apply 和 $digest 的區(qū)別)

為什們呢?

假設沒有AngularJS,要讓我們自己實現(xiàn)這個類似的功能,該怎么做呢?

<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();
}

測試一下:

 AngularJS的臟檢查深入分析

可以看到我們沒有直接使用DOM的onclick方法,而是搞了一個ng-click,然后在bind里面把這個ng-click對應的函數(shù)拿出來,綁定到onclick的事件處理函數(shù)中。為什么要這樣呢?因為數(shù)據(jù)雖然變更了,但是還沒有往界面上填充,我們需要在此做一些附加操作。

另外,由于雙向綁定機制,在DOM操作中,雖然更新了數(shù)據(jù)的值,但是并沒有立即反映到界面上,而是通過 apply() 來反映到界面上,從而完成職責的分離,可以認為是單一職責模式了。

在真正的Angular中,ng-click 封裝了click,然后調用一次 apply 函數(shù),把數(shù)據(jù)呈現(xiàn)到界面上

在Angular 的apply函數(shù)中,這里先進行臟檢測,看 oldValue 和 newVlue 是否相等,如果不相等,那么講newValue 反饋到界面上,通過如果通過 $watch 注冊了 listener事件,那么就會調用該事件。

臟檢查的優(yōu)缺點

經過我們上面的分析,可以總結:

  1. 簡單理解,一次臟檢查就是調用一次 $apply() 或者 $digest(),將數(shù)據(jù)中最新的值呈現(xiàn)在界面上。
  2. 而每次 UI 事件變更,ajax 還有 timeout 都會觸發(fā) $apply()。

然而就有了接下來的討論?

不斷觸發(fā)臟檢查是不是一種好的方式?
 有很多人認為,這樣對性能的損耗很大,不如 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++;
 }
 }
}

在臟檢測的機制下,這個過程毫無壓力,會等待到 循環(huán)執(zhí)行結束,然后一次更新 checkedItemsNumber,應用到界面上。 但是在基于setter的機制就慘了,每變化一次checkedItemsNumber就需要更新一次,這樣性能就會極低。
 所以說,兩種不同的監(jiān)控方式,各有其優(yōu)缺點,最好的辦法是了解各自使用方式的差異,考慮出它們性能的差異所在,在不同的業(yè)務場景中,避開最容易造成性能瓶頸的用法。

好了,現(xiàn)在已經了解了雙向數(shù)據(jù)綁定了 臟檢查的觸發(fā)機制,那么,臟檢查內部又是怎么實現(xiàn)的呢?

臟檢查的內部實現(xiàn)

首先,構造$scope 對象,

function $scope = function(){}

現(xiàn)在,我們回到開頭 $watch。

我們說,每一個綁定到UI上的數(shù)據(jù)都有擁有一個對應的$watch 對象,這個對象會被push到watchList中。

它擁有兩個函數(shù)作為屬性

  1. getNewValue() 也叫監(jiān)控函數(shù),勇于在值發(fā)生變化后得到提示,并返回新值。
  2. listener() 監(jiān)聽函數(shù),用于在數(shù)據(jù)變更的時候響應行為。

還有一個字符串屬性

name: 當前watch作用的變量名

function $scope(){
 this. $$watchList = [];
}

在Angular框架中,雙美元符前綴$$表示這個變量被當作私有的來考慮,不應當在外部代碼中調用。

現(xiàn)在我們可以定義$watch方法了。它接受兩個函數(shù)作參數(shù),把它們存儲在$$watchers數(shù)組中。我們需要在每個Scope實例上存儲這些函數(shù),所以要把它放在Scope的原型上:

$scope.prototype.$watch = function(name,getNewValue,listener){
 var watch = {
  name:name,
  getNewValue : getNewValue,
  listener : listener
 };

 this.$$watchList.push(watch);
}

另外一面就是$digest函數(shù)。它執(zhí)行了所有在作用域上注冊過的監(jiān)聽器。我們來實現(xiàn)一個它的簡化版,遍歷所有監(jiān)聽器,調用它們的監(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)聽器并且運行臟檢查了。

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();

AngularJS的臟檢查深入分析

代碼會托管到github,測試文件路徑跟命令中路徑一致

OK,兩個監(jiān)聽均已經觸發(fā)。

這些本身沒什么大用,我們要的是能檢測由getNewValue返回指定的值是否確實變更了,然后調用監(jiān)聽函數(shù)。

那么,我們需要在getNewValue() 上每次都得到數(shù)據(jù)上最新的值,所以需要得到當前的scope對象

getNewValue = function(scope){
 return scope[this.name];
}

是監(jiān)控函數(shù)的一般形式:從作用域獲取一些值,然后返回。

$digest函數(shù)的作用是調用這個監(jiān)控函數(shù),并且比較它返回的值和上一次返回值的差異。如果不相同,監(jiān)聽器就是臟的,它的監(jiān)聽函數(shù)就應當被調用。

想要這么做,$digest需要記住每個監(jiān)控函數(shù)上次返回的值。既然我們現(xiàn)在已經為每個監(jiān)聽器創(chuàng)建過一個對象,只要把上一次的值存在這上面就行了。下面是檢測每個監(jiān)控函數(shù)值變更的$digest新實現(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);
  // 在第一次渲染界面,進行一個數(shù)據(jù)呈現(xiàn).
  var oldValue = watch.last;
  if(newValue!=oldValue){
   watch.listener(newValue,oldValue);
  }
  watch.last = newValue;
 }
}

對于每一個watch,我們使用 getNewValue() 并且把scope實例 傳遞進去,得到數(shù)據(jù)最新值 。然后和上一次值進行比較,如果不同,那就調用 getListener,同時把新值和舊值一并傳遞進去。 最終,我們把last 屬性設置為新返回的值,也就是最新值。

這個$digest 再一次調用,last 為undefined,所以一定會進行一次數(shù)據(jù)呈現(xiàn)。

好了,我們看看這個監(jiān)控函數(shù)如何運行的

var scope = new $scope();
scope.hello = 10;
scope.$watch('hello', function(scope) {
 // 注意,要理解這里的this ,這個函數(shù)實際是 var newValue = watch.getNewValue(this); 這樣調用,那么 this 就指的是當前監(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();
 

運行結果

AngularJS的臟檢查深入分析

我們已經實現(xiàn)了Angular作用域的本質:添加監(jiān)聽器,在digest里運行它們。

也已經可以看到幾個關于Angular作用域的重要性能特性:

  1. 在作用域上添加數(shù)據(jù)本身并不會有性能折扣。如果沒有監(jiān)聽器在監(jiān)控某個屬性,它在不在作用域上都無所謂。Angular并不會遍歷作用域的屬性,它遍歷的是監(jiān)聽器。一旦將數(shù)據(jù)綁定到UI上,就會添加一個監(jiān)聽器。
  2. $digest里會調用每個getNewValue(),因此,最好關注監(jiān)聽器的數(shù)量,還有每個獨立的監(jiān)控函數(shù)或者表達式的性能。

有時候并不需要注冊那么多的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);
  // 在第一次渲染界面,進行一個數(shù)據(jù)呈現(xiàn).
  var oldValue = watch.last;
  if(newValue!=oldValue){
   watch.listener(newValue,oldValue);
  }
  watch.last = newValue;
 }
}

我們這樣做,就要求每個監(jiān)聽器watch 都必須注冊 listener,然而事實是:在Angular 應用中,只有少數(shù)的監(jiān)聽器需要注冊listener。

更改 $scope.prototype.$wacth,在這里放置一個空的函數(shù)。

$scope.prototype.$watch = function(name,getNewValue,listener){
 var watch = {
  name:name,
  getNewValue : getNewValue,
  listener : listener || function(){}
 };

 this.$$watchList.push(watch);
}

貌似這樣已經初步理解了臟檢查原理,但是一個重要的問題我們忽視了。

先后注冊了兩個監(jiān)聽器,第二個監(jiān)聽器的listener 改變了 第一個監(jiān)聽器對應數(shù)據(jù)的值,那么這么做會檢測的到嗎?

看下面的例子

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);

 AngularJS的臟檢查深入分析

可以看到,值為 8,1,已經發(fā)生改變,但是界面上的值卻沒有改變。

現(xiàn)在來修復這個問題。

當數(shù)據(jù)臟的時候持續(xù)Digest

我們需要改變一下digest,讓它持續(xù)遍歷所有監(jiān)聽器,直到監(jiān)控的值停止變更。

首先,我們把現(xiàn)在的$digest函數(shù)改名為$$digestOnce,它把所有的監(jiān)聽器運行一次,返回一個布爾值,表示是否還有變更了。

$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操作,已經檢查過的數(shù)據(jù)可能變臟
  dirty = true;
 }

  watch.last = newValue;
  return dirty;
 }
};

然后,我們重新定義$digest,它作為一個“外層循環(huán)”來運行,當有變更發(fā)生的時候,調用$$digestOnce:

$scope.prototype.$digest = function() {
 var dirty = true;
 while(dirty) {
 dirty = this.$$digestOnce();
 } 
};

$digest現(xiàn)在至少運行每個監(jiān)聽器一次了。如果第一次運行完,有監(jiān)控值發(fā)生變更了,標記為dirty,所有監(jiān)聽器再運行第二次。這會一直運行,直到所有監(jiān)控的值都不再變化,整個局面穩(wěn)定下來了。

在Angular作用域里并不是真的有個函數(shù)叫做$$digestOnce,相反,digest循環(huán)都是包含在$digest里的。我們的目標更多是清晰度而不是性能,所以把內層循環(huán)封裝成了一個函數(shù)。

測試一下

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);

AngularJS的臟檢查深入分析 

可以看到,現(xiàn)在界面上的數(shù)據(jù)已經全部為最新

我們現(xiàn)在可以對Angular的監(jiān)聽器有另外一個重要認識:它們可能在單次digest里面被執(zhí)行多次。這也就是為什么人們經常說,監(jiān)聽器應當是冪等的:一個監(jiān)聽器應當沒有邊界效應,或者邊界效應只應當發(fā)生有限次。比如說,假設一個監(jiān)控函數(shù)觸發(fā)了一個Ajax請求,無法確定你的應用程序發(fā)了多少個請求。

如果兩個監(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án)下去。如何解決呢?

更穩(wěn)定的 $digest

我們要做的事情是,把digest的運行控制在一個可接受的迭代數(shù)量內。如果這么多次之后,作用域還在變更,就勇敢放手,宣布它永遠不會穩(wěn)定。在這個點上,我們會拋出一個異常,因為不管作用域的狀態(tài)變成怎樣,它都不太可能是用戶想要的結果。

迭代的最大值稱為TTL(short for Time To Live)。這個值默認是10,可能有點小(我們剛運行了這個digest 100,000次?。?,但是記住這是一個性能敏感的地方,因為digest經常被執(zhí)行,而且每個digest運行了所有的監(jiān)聽器。用戶也不太可能創(chuàng)建10個以上鏈狀的監(jiān)聽器。

我們繼續(xù),給外層digest循環(huán)添加一個循環(huán)計數(shù)器。如果達到了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();

AngularJS的臟檢查深入分析

好了,關于 Angular 臟檢查和 雙向數(shù)據(jù)綁定原理就介紹到這里,雖然離真正的Angular 還差很多,但是也能基本解釋原理了。希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI