原文鏈接:
Angular.js’ $digest is reborn in the newer version of Angular
我運用 Angular.js 框架好些年了,只管它飽受指摘,但我依舊以為它是個難以想象的框架。我是從這本書 Building your own Angular.js 最先進修的,而且讀了框架的大批源碼,所以我以為本身對 Angular.js 內部機制比較相識,而且對建立這個框架的架構頭腦也比較熟習。近來我在試圖控制新版 Angular 框架內部架構頭腦,並與舊版 Angular.js 內部架構頭腦舉行比較。我發明並非像網上說的那樣,恰恰相反,Angular 大批自創了 Angular.js 的設想頭腦。
其中之一就是名聲蹩腳的 digest loop:
這個設想的重要題目就是本錢太高。轉變遞次中的任何事物,須要實行成百上千個函數去查詢哪一個數據發作變化。而這是 Angular 的基本部份,然則它會把查詢限定在部份 UI 上,從而進步機能。
假如能更好明白 Angular 是怎樣完成 digest 的,便可能把你的遞次設想的更高效,比方,運用 $scope.$digest()
而不是 $scope.$apply
,或許運用不可變對象。但事實是,為了設想出更高效的遞次,從而去明白框架內部完成,這可能對許多人來講不是簡樸的事變。
所以大批有關 Angular 的文章教程里都聲稱框架里不會再有 $digest cycle
了。這取決於對 digest 觀點怎樣明白,但我以為這很有誤導性,因為它依然存在。確實,在 Angular 里沒有 scopes 和 watchers
,也不再須要挪用 $scope.$digest()
,然則檢測數據變化的機制依舊是遍歷全部組件樹,隱式挪用 watchers
,然後更新 DOM。所以實際上是完整重寫了,但被優化增強了,關於新的查詢機制能夠檢察我寫的 Everything you need to know about change detection in Angular。
digest 的必要性
最先前讓我們先回想下 Angular.js 中為什麼存在 digest
。統統框架都是在處置懲罰數據模子(JavaScript Objects)和 UI(Browser DOM)的同步題目,最大困難是怎樣曉得什麼時刻數據模子發作轉變,而查詢數據模子什麼時刻發作轉變的歷程就是變動檢測(change detection)。這個題目的差別完成設計也是如今浩瀚前端框架的最大區分點。我設計寫篇文章,有關差別框架變動檢測完成的比較,假如你感興趣並願望收到關照,能夠關注我。
有兩種體式格局來檢測變化:須要運用者關照框架;經由過程比較來自動檢測變化。
假定我們有以下一個對象:
let person = {name: 'Angular'};
然後我們去更新 name
屬性值,然則框架是怎樣曉得這個值什麼時刻被更新呢?一種體式格局是須要運用者通知框架(注:如 React 體式格局):
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
為(注:為清楚明白,不翻譯):
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
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 並沒有相似 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.js 體系外的第三方組件
在 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();
})
}
}