原文連接:Angular.js’ $digest is reborn in the newer version of Angularhtml
我使用 Angular.js 框架好些年了,儘管它飽受批評,但我依然以爲它是個難以想象的框架。我是從這本書 Building your own Angular.js 開始學習的,而且讀了框架的大量源碼,因此我以爲本身對 Angular.js 內部機制比較瞭解,而且對建立這個框架的架構思想也比較熟悉。最近我在試圖掌握新版 Angular 框架內部架構思想,並與舊版 Angular.js 內部架構思想進行比較。我發現並非像網上說的那樣,偏偏相反,Angular 大量借鑑了 Angular.js 的設計思想。前端
其中之一就是名聲糟糕的 digest loop:node
這個設計的主要問題就是成本過高。改變程序中的任何事物,須要執行成百上千個函數去查詢哪一個數據發生變化。而這是 Angular 的基礎部分,可是它會把查詢限定在部分 UI 上,從而提升性能。git
若是能更好理解 Angular 是如何實現 digest 的,就可能把你的程序設計的更高效,好比,使用 $scope.$digest()
而不是 $scope.$apply
,或者使用不可變對象。但事實是,爲了設計出更高效的程序,從而去理解框架內部實現,這可能對不少人來講不是簡單的事情。angularjs
因此大量有關 Angular 的文章教程裏都宣稱框架裏不會再有 $digest cycle
了。這取決於對 digest 概念如何理解,但我認爲這頗有誤導性,由於它仍然存在。的確,在 Angular 裏沒有 scopes 和 watchers
,也再也不須要調用 $scope.$digest()
,可是檢測數據變化的機制依然是遍歷整個組件樹,隱式調用 watchers
,而後更新 DOM。因此其實是徹底重寫了,但被優化加強了,關於新的查詢機制能夠查看我寫的 Everything you need to know about change detection in Angular。github
開始前讓咱們先回憶下 Angular.js 中爲什麼存在 digest
。全部框架都是在解決數據模型(JavaScript Objects)和 UI(Browser DOM)的同步問題,最大難題是如何知道何時數據模型發生改變,而查詢數據模型什麼時候發生改變的過程就是變動檢測(change detection)。這個問題的不一樣實現方案也是如今衆多前端框架的最大區別點。我計劃寫篇文章,有關不一樣框架變動檢測實現的比較,若是你感興趣並但願收到通知,能夠關注我。api
有兩種方式來檢測變化:須要使用者通知框架;經過比較來自動檢測變化。bash
假設咱們有以下一個對象:前端框架
let person = {name: 'Angular'};
複製代碼
而後咱們去更新 name
屬性值,可是框架是怎麼知道這個值什麼時候被更新呢?一種方式是須要使用者告訴框架(注:如 React 方式):angular2
constructor() {
let person = {name: 'Angular'};
this.state = person;
}
...
// explicitly notifying React about the changes
// and specifying what is about to change
this.setState({name: 'Changed'});
複製代碼
或者強迫用戶去封裝該屬性,從而框架能添加 setters
(注:如 Vue 方式):
let app = new Vue({
data: {
name: 'Hello Vue!'
}
});
// the setter is triggered so Vue knows what changed
app.name = 'Changed';
複製代碼
另外一種方式是保存 name
屬性的上一個值,並與當前值進行比較:
if (previousValue !== person.name) // change detected, update DOM
複製代碼
可是何時結束比較呢?咱們應該在每一次異步代碼運行時都去檢查,因爲這部分運行的代碼是做爲異步事件去處理,即所謂的 Virtual Machine(VM) turn/tick(注:Virtual Machine 的理解可參考 VM),因此能夠緊接着在 VM turn 的後面,執行數據變化檢查代碼。這也是爲什麼 Angular.js 使用 digest
,因此咱們能夠定義 digest
爲(注:爲清晰理解,不翻譯):
a change detection mechanism that walks the tree of components, checks each component for changes and updates DOM when a component property is changed。
若是咱們這麼去定義 digest
的話,那我能夠說數據變化檢查機制的主要部分在 Angular 裏沒有變化,變化的是 digest
的實現。
Angular.js 使用 watcher
和 listener
的概念,watcher
就是一個返回被監測值的函數,大多數時候這個被監測值就是數據模型的屬性。但也不老是數據模型屬性,如咱們能夠在做用域裏追蹤組件狀態,計算屬性值,第三方組件等等。若是當前返回值與先前值不一樣,Angular.js 就會調用 listener
,而 listener
一般用來更新 UI。
$watch
函數的參數列表以下:
$watch(watcher, listener);
複製代碼
因此,若是咱們有一個帶有name
屬性的 person
對象,並在模板裏這樣使用 <span>{{name}}</span>
,那就能夠像這樣去追蹤這個屬性變化從而更新 DOM:
$watch(() => {
return person.name
}, (value) => {
span.textContent = value
});
複製代碼
這與插值和 ng-bind
類的指令本質上作的同樣,Angular.js 使用指令來映射 DOM 的數據模型。可是 Angular 再也不這麼去作,它使用屬性映射來鏈接數據模型和 DOM。上面的示例在 Angular 會這麼實現:
<span [textContent]="person.name"></span>
複製代碼
因爲存在不少組件,並組成了組件樹,每個組件都有着不一樣的數據模型,因此就存在分層的 watchers
,與分層的組件樹很類似。儘管使用做用域把 watchers
組合在一塊兒,但它們並不相關。
如今,在 digest
期間,Angular.js 會遍歷 watchers
樹並更新 DOM。若是你使用 $timeout
,$http
或根據須要使用 $scope.$apply
和 $scope.$digest
等方式,就會在每一次異步事件中觸發 digest cycle
。
watchers
是嚴格按照順序觸發:首先是父組件,而後是子組件。這頗有意義,但卻有着不受歡迎的缺點。一個被觸發的 watcher listener
有不少反作用,好比包括更新父組件的屬性。若是父監聽器已經被觸發了,而後子監聽器又去更新父組件屬性,那這個變化不會被檢測到。這就是爲什麼 digest loop
要運行屢次來獲取穩定的程序狀態,即確保沒有數據再發生變化。運行次數最大限定爲 10 次,這個設計如今被認爲是有缺陷的,而且 Angular 不允許這樣作。
Angular 並無相似 Angular.js 中 watcher
概念,可是追蹤模型屬性的函數依然存在。這些函數是由框架編譯器生成的,而且是私有不可訪問的。另外,它們也和 DOM 緊密耦合在一塊兒,這些函數就存儲在生成視圖結構 ViewDefinition 的 updateRenderer 中。
它們也很特別:只追蹤模型變化,而不是像 Angular.js 追蹤一切數據變化。每個組件都有一個 watcher
來追蹤在模板中使用的組件屬性,並對每個被監聽的屬性調用 checkAndUpdateTextInline 函數。這個函數會比較屬性的上一個值與當前值,若是有變化就更新 DOM。
好比,AppComponent
組件的模板:
<h1>Hello {{model.name}}</h1>
複製代碼
Angular Compiler 會生成以下相似代碼:
function View_AppComponent_0(l) {
// jit_viewDef2 is `viewDef` constructor
return jit_viewDef2(0,
// array of nodes generated from the template
// first node for `h1` element
// second node is textNode for `Hello {{model.name}}`
[
jit_elementDef3(...),
jit_textDef4(...)
],
...
// updateRenderer function similar to a watcher
function (ck, v) {
var co = v.component;
// gets current value for the component `name` property
var currVal_0 = co.model.name;
// calls CheckAndUpdateNode function passing
// currentView and node index (1) which uses
// interpolated `currVal_0` value
ck(v, 1, 0, currVal_0);
});
}
複製代碼
注:使用 Angular-CLI
ng new
一個新項目,執行ng serve
運行程序後,就可在 Chrome Dev Tools 的 Source Tab 的ng://
域下查看到編譯組件後生成的**.ngfactory.js
文件,即上面相似代碼。
因此,即便 watcher
實現方式不一樣,但 digest loop
仍然存在,僅僅是換了名字爲 change detection cycle (注: 爲清晰理解,不翻譯):
In development mode, tick() also performs a second change detection cycle to ensure that no further changes are detected.
上文說到在 digest
期間,Angular.js 會遍歷 watchers
樹並更新 DOM,這與 Angular 中機制很是相似。在變動檢測循環期間(注:與本文中 digest cycle
相同概念),Angular 也會遍歷組件樹並調用渲染函數更新 DOM。這個過程是 checking and updating view process 過程的一部分,我也寫了一篇長文 Everything you need to know about change detection in Angular 。
就像 Angular.js 同樣,在 Angular 中變動檢測也一樣是由異步事件觸發(注:如異步請求數據返回事件;用戶點擊按鈕事件;setTimeout/setInterval
)。可是因爲 Angular 使用 zone 包來給全部異步事件打補丁,因此對於大部分異步事件來講,不須要手動觸發變動檢測。Angular 框架會訂閱 onMicrotaskEmpty 事件,並在一個異步事件完成時會通知 Angular 框架,而這個 onMicrotaskEmpty 事件是在當前 VM Turn 的 microtasks
隊列裏不存在任務時被觸發。然而,變動檢測也能夠手動方式觸發,如使用 view.detectChanges 或 ApplicationRef.tick (注:view.detectChanges
會觸發當前組件及子組件的變動檢測,ApplicationRef.tick
會觸發整個組件樹即全部組件的變動檢測)。
Angular 強調所謂的單向數據流,從頂部流向底部。在父組件完成變動檢測後,低層級裏的組件,即子組件,不允許改變父組件的屬性。但若是一個組件在 DoCheck 生命週期鉤子裏改變父組件屬性,倒是能夠的,由於這個鉤子函數是在**更新父組件屬性變化以前調用的**(注:即第 6 步 DoCheck, 在 第 9 步 updates DOM interpolations for the current view if properties on current view component instance changed 以前調用)。可是,若是改變父組件屬性是在其餘階段,好比 AfterViewChecked 鉤子函數階段,在父組件已經完成變動檢測後,再去調用這個鉤子函數,在開發者模式下框架會拋出錯誤:
Expression has changed after it was checked
關於這個錯誤,你能夠讀這篇文章 Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error 。(注:這篇文章已翻譯)
在生產環境下 Angular 不會拋出錯誤,可是也不會檢查數據變化直到下一次變動檢測循環。(注:由於開發者模式下 Angular 會執行兩次變動檢測循環,第二次檢查會發現父組件屬性被改變就會拋出錯誤,而生產環境下只執行一次。)
在 Angular.js 裏,每個組件定義了一堆 watchers
來追蹤以下數據變化:
在 Angular 裏倒是這麼實現這些功能的:可使用 OnChanges 生命週期鉤子函數來監聽父組件屬性;可使用 DoCheck 生命週期鉤子來監聽當前組件屬性,由於這個鉤子函數會在 Angular 處理當前組件屬性變化前去調用,因此能夠在這個函數裏作任何須要的事情,來獲取即將在 UI 中顯示的改變值;也可使用 OnInit 鉤子函數來監聽第三方組件並手動運行變動檢測循環。
好比,咱們有一個顯示當前時間的組件,時間是由 Time
服務提供,在 Angular.js 中是這麼實現的:
function link(scope, element) {
scope.$watch(() => {
return Time.getCurrentTime();
}, (value) => {
$scope.time = value;
})
}
複製代碼
而在 Angular 中是這麼實現的:
class TimeComponent {
ngDoCheck()
{
this.time = Time.getCurrentTime();
}
}
複製代碼
另外一個例子是若是咱們有一個沒集成在 Angular 系統內的第三方 slider
組件,但咱們須要顯示當前 slide,那就僅僅須要把這個組件封裝進 Angular 組件內,監聽 slider's changed
事件,並手動觸發變動檢測循環來同步 UI。Angular.js 裏這麼寫:
function link(scope, element) {
slider.on('changed', (slide) => {
scope.slide = slide;
// detect changes on the current component
$scope.$digest();
// or run change detection for the all app
$rootScope.$digest();
})
}
複製代碼
Angular 裏也一樣原理(注:也一樣須要手動觸發變動檢測循環,this.appRef.tick()
會檢測全部組件,而 this.cd.detectChanges()
會檢測當前組件及子組件):
class SliderComponent {
ngOnInit() {
slider.on('changed', (slide) => {
this.slide = slide
// detect changes on the current component
// this.cd is an injected ChangeDetector instance
this.cd.detectChanges();
// or run change detection for the all app
// this.appRef is an ApplicationRef instance
this.appRef.tick();
})
}
}
複製代碼