溫馨提示×

溫馨提示×

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

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

angular臟檢查原理及偽代碼實現

發布時間:2021-06-03 16:09:22 來源:億速云 閱讀:227 作者:Leah 欄目:web開發

本篇文章為大家展示了angular臟檢查原理及偽代碼實現,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

angular的數據響應機制

那么,在代碼層面,angular是怎么做到監聽數據變動然后更新界面的呢?答案是,angular根本不監聽數據的變動,而是在恰當的時機從$rootScope開始遍歷所有$scope,檢查它們上面的屬性值是否有變化,如果有變化,就用一個變量dirty記錄為true,再次進行遍歷,如此往復,直到某一個遍歷完成時,這些$scope的屬性值都沒有變化時,結束遍歷。由于使用了一個dirty變量作為記錄,因此被稱為臟檢查機制。

這里面有三個問題:

  1. “恰當的時機”是什么時候?

  2. 如何做到知道屬性值是否有變化?

  3. 這個遍歷循環是怎么實現的?

要解決這三個問題,我們需要深入了解angular的$watch, $apply, $digest。

$watch綁定要檢查的值

簡單的說,當一個作用域創建的時候,angular會去解析模板中當前作用域下的模板結構,并且自動將那些插值(如{{text}})或調用(如ng-click="update")找出來,并利用$watch建立綁定,它的回調函數用于決定如果新值和舊值不同時(或相同時)要干什么事。當然,你也可以手動在腳本里面使用$scope.$watch對某個屬性進行綁定。它的使用方法如下:

$scope.$watch(string|function, listener, objectEquality, prettyPrintExpression)

第一個參數是一個字符串或函數,如果是函數,需要運行后得到一個字符串,這個字符串用于確定將綁定$scope上的哪個屬性。listener則是回調函數,表示當這個屬性的值發生變化時,執行該函數。objectEquality是一個boolean,為true的時候,會對object進行深檢查(懂什么叫深拷貝的話就懂深檢查)。第四個參數是如何解析第一個參數的表達式,使用比較復雜,一般不傳。

$digest遍歷遞歸

當使用$watch綁定了要檢查的屬性之后,當這個屬性發生變化,就會執行回調函數。但是前面已經說過了,angular里面沒有監聽這么一說,那么它怎么會被回調呢?它沒有用object的setter機制,而是臟檢查機制。臟檢查的核心,就是$digest循環。當用戶執行了某些操作之后,angular內部會調用$digest(),最終導致界面重新渲染。那么它究竟是怎么一回事呢?

調用$watch之后,對應的信息被綁定到angular內部的一個$$watchers中,它是一個隊列(數組),而當$digest被觸發時,angular就會去遍歷這個數組,并且用一個dirty變量記錄$$watchers里面記錄的那些$scope屬性是否有變化,當有變化的時候,dirty被設置為true,在$digest執行結束的時候,它會再檢查dirty,如果dirty為true,它會再調用自己,直到dirty為true。但是為了防止死循環,angular規定,當遞歸發生了10次或以上時,直接拋出一個錯誤,并跳出循環。

遞歸流程如下:

  1. 判斷dirty是否為true,如果為false,則不進行$digest遞歸。(dirty默認為true)

  2. 遍歷$$watchers,取出對應的屬性值的老值和新值

  3. 根據objectEquality進行新老值的對比。

  4. 如果兩個值不同,則繼續往下執行。如果兩個值相同,則設置dirty為false。

  5. 檢查完所有的watcher之后,如果dirty還為true(這一點需要閱讀我下面的偽代碼)

  6. 設置dirty為true

  7. 用新值代替老值,這樣,在下一輪遞歸的時候,老值就是這一輪的新值

  8. 再次調用$digest

當遞歸流程結束之后,$digest還要執行:

將變化后的$scope重新渲染到界面

當一個作用域創建完之后,$scope.$digest會被運行一次。dirty的默認值被設定為true,因此,如果你在controller里面使用了$watch,并且進行了屬性賦值,往往刷新頁面就可以看到$watch的回調函數被執行了。但是,現在問題來了,上面說的“angular內部會調用$digest()”,這個內部是怎么實現的?

$apply觸發$digest

在我們自己編程時,并不直接使用$digest,而是調用$scope.$apply(),$apply內部會觸發$digest遞歸遍歷。同時,你可以給$apply傳一個參數,是個函數,這個函數會在$digest開始之前執行?,F在回到上面的問題,angular內部怎么觸發$digest?實際上,angular里面要求你通過ng-click, ng-modal, ng-keyup等來進行數據的雙向綁定,為什么,因為這些angular的內部指令封裝了$apply,比如ng-click,它其實包含了document.addEventListener('click')和$scope.$apply()。

當用戶在模板里面使用ng-click時,如下:

<div ng-click="update()">change</div>
$scope.update = function() {
 $scope.name = 'tom'
}

實際上,當用戶點擊之后,angular內部還會執行$scope.$apply(),從而觸發$digest遍歷遞歸,最終觸發界面重繪。

手動調用$apply

但是有些情況下,我們不可能直接使用angular內部指令,有兩種情況我們需要手動調用$apply,一種是調用angular內置的語法糖,比如$http, $timeout,另一種是我們沒有使用angular內部機制去更新了$scope,比如我們用$element.on('click', () => $scope.name = 'lucy')。也就是說“異步”和“機制外”修改$scope屬性值之后,我們都要手動調用$apply,雖然我們在調用$timeout的時候,沒有手寫$apply,但實際上它內部確實調用了$apply:

function($timeout) {
 // 當我們通過on('click')的方式觸發某些更新的時候,可以這樣做
 $timeout(() => {
  $scope.name = 'lily'
 })
 // 也可以這樣做
 $element.on('click', () => {
  $scope.name = 'david'
  $scope.$apply()
 })
}

但是,一定要注意,在遞歸過程中,絕對不能手動調用$apply,比如在ng-click的函數中,比如在$watch的回調函數中。

偽代碼實現

通過上面的講解,你可能已經對angular里面的臟檢查已經了解了,但是我們還是希望更深入,用代碼來把事情說清楚。我這里不去抄寫angular的源碼,而是自己寫一段偽代碼,這樣更有助于理解整個機制。

import { isEqual } from 'lodash'

class Scope {
 constructor() {
  this.$$dirty = true
  this.$$count = 0
  this.$$watchers = []
 }
 $watch(property, listener, deepEqual) {
  let watcher = {
   property,
   listener,
   deepEqual,
  }
  this.$$watchers.push(watcher)
 }
 $digest() {
  if (this.$$count >= 10) {
   throw new Error('$digest超過10次')
  }

  this.$$watchers.forEach(watcher => {
   let newValue = eval('return this.' + watcher.property)
   let oldValue = watcher.oldValue
   if (watcher.deepEqual && isEqual(newValue, oldValue)) {
    watcher.dirty = false
   } 
   else if (newValue === oldValue) {
    watcher.dirty = false
   }
   else {
    watcher.dirty = true
    eval('this.' + watcher.property + ' = ' newValue)
    watcher.listener(newValue, oldValue) // 注意,listener是在newValue賦值給$scope之后執行的
    watcher.oldValue = newValue
   }
   // 這里的實現和angular邏輯里面有一點不同,angular里面,當newValue和oldValue都為undefined時,listener會被調用,可能是angular里面在$watch的時候,會自動給$scope加上原本沒有的屬性,因此認為是一次變動
  })
  
  this.$$count ++

  this.$$dirty = false
  for (let watcher of this.$$watchers) {
   if (watcher.dirty) {
    this.$$dirty = true
    break
   }
  }

  if (this.$$dirty) {
   this.$digest()
  }
  else {
    this.$patch()
    this.$$dirty = true
    this.$$count = 0
  }
 }
 $apply() {
  if (this.$$count) {
   return // 當$digest執行的過程中,不能觸發$apply
  }
  this.$$dirty = true
  this.$$count = 0
  this.$digest()
 }
 $patch() {
  // 重繪界面
 }
}
function ControllerRegister(controllerTemplate, controllerFunction) {
 let $scope = new Scope()
 $paser(controllerTemplate, $scope) // 解析controller的模板,把模板中的屬性全部都解析出來,并且把這些屬性賦值給$scope
 controllerFunction($scope) // 在controllerFunction內部可能又給$scope添加了一些屬性,注意,不能在運行controllerFunction的時候調用$scope.$apply()

 let properties = Object.keys($scope) // 找出$scope上的所有屬性
 // 要把$scope上的一些內置屬性排除掉 
 properties = properties.filter(item => item.indexOf('$') !== 0) // 當然,這種排除方法只能保證在用戶不使用$作為屬性開頭的時候有用

 properties.forEach(property => {
  $scope.$watch(property, () => {}, true)
 })

 $scope.$digest()
}

上述內容就是angular臟檢查原理及偽代碼實現,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。

向AI問一下細節

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

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女