[譯] $digest 在 Angular 中重生

原文鏈接:
Angular.js’ $digest is reborn in the newer version of Angular

《[譯] $digest 在 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 運用 watcherlistener 的觀點,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 嚴密耦合在一起,這些函數就存儲在天生視圖構造 ViewDefinitionupdateRenderer 中。

它們也很迥殊:只追蹤模子變化,而不是像 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.detectChangesApplicationRef.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();
        })
    }
}
    原文作者:lx1036
    原文地址: https://segmentfault.com/a/1190000014687848
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞