首頁 > web前端 > js教程 > 主體

透過範例入手聊聊Angular中的變更檢測

青灯夜游
發布: 2022-02-08 10:38:36
轉載
1569 人瀏覽過

這篇文章帶大家了解一下Angular中的變更檢測,先從一個小的範例入手,然後逐步展開深入聊聊變更檢測,希望對大家有所幫助!

透過範例入手聊聊Angular中的變更檢測

Angular 中的變更偵測是一種用來將應用程式 UI 的狀態與資料的狀態同步的機制。當應用邏輯變更元件資料時,綁定到視圖中 DOM 屬性上的值也要隨之變更。變更偵測器負責更新視圖以反映目前的資料模型。 【相關教學推薦:《angular教學》】

紙上得來終覺淺,絕知此事要躬行。為了讓讀者朋友更容易理解,本文先從一個小小的範例入手,然後逐步展開。範例如下:

// app.component.ts
import { Component } from '@angular/core';
@Component({  selector: 'app-root', 
    templateUrl: './app.component.html', 
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    title = 'aa';
    handleClick() {
        this.title = 'bb';
    }}
// app.componnet.html
<div (click)="handleClick()">{{title}}</div>
登入後複製

範例比較簡單,就是給div元素綁定了一個點擊事件,點擊該元素就會改變變數title的值,介面的顯示也會隨之更新。框架如何知道什麼時候需要更新視圖,以及如何更新視圖的呢?我們來一探究竟。

當我們點選div元素時,handleClick函數會被執行。那麼在 Angular 應用中該函數是如何被觸發執行的呢?如果你看過我之前的關於zone.js介紹的文章就會知道,Angular 應用程式中點擊事件已經被zone.js接手。基於此答案便顯而易見,最開始肯定是被zone.js觸發執行,但在這裡我還們還要進一步分析直接調用關係進而層層展開。最靠近handleClick函數呼叫的是下面的程式碼:

function wrapListener(listenerFn, ...) {
    return function wrapListenerIn_markDirtyAndPreventDefault(e) {
        let result = executeListenerWithErrorHandling(listenerFn, ...);
    }
}
登入後複製

上述程式碼中listenerFn函數指向的便是handleClick#,但它又是wrapListener函數的參數。範例中元素綁定點擊事件,相關模板編譯產物大概是這樣:

function AppComponent_Template(rf, ctx) { 
    ...... 
    i0["ɵɵlistener"]("click", function AppComponent_Template_div_click_0_listener() {
    return ctx.handleClick();
    })
}
登入後複製

初次載入應用程式會依序執行renderView、然後執行executeTemplate,接著便觸發了上述的模板函數,就這樣元素的點擊函數便一路傳遞到了listenerFn參數。到這裡我們了解了,點擊函數的觸發源頭是zone.js,真實的點擊函數傳遞卻是由Angular 實現,那麼zone.js和Angular 是如何關聯的呢? zone.js會為每個非同步事件安排一個任務,結合本文範例來說,invokeTask便是由下面程式碼呼叫:

function forkInnerZoneWithAngularBehavior(zone) {
    zone._inner = zone._inner.fork({
    name: &#39;angular&#39;,
    properties: { &#39;isAngularZone&#39;: true },
    onInvokeTask: (delegate, current, target, task, applyThis, applyArgs) => {
        try {
            onEnter(zone);
            return delegate.invokeTask(target, task, ...);
        }
        finally {
            onLeave(zone);
        }
    }
    })
}
登入後複製

看到這裡是不是就很熟悉了,因為在之前的zone.js介紹的文章裡,便有類似的程式碼片段。而forkInnerZoneWithAngularBehavior函數又是由類別 NgZone 的建構子呼叫。至此我們引出了 Angular 變更偵測的一個主角 NgZone,它是對zone.js的一個簡單封裝。

現在我們知道範例中點擊函數是如何被執行的,那麼函數執行了以後應用資料有變化了,視圖又是如何及時更新的呢?我們還是回到上面提到的forkInnerZoneWithAngularBehavior函數中,try finally語句區塊中,執行了invokeTask函數最終還會執行onLeave(zone )函數。再往下分析就能看到onLeave函數最終呼叫了checkStable函數:

function checkStable(zone) {
    zone.onMicrotaskEmpty.emit(null);
}
登入後複製

對應地在類別ApplicationRef建構子中訂閱了這個emit事件:

class ApplicationRef {
    /** @internal */
    constructor() {
        this._zone.onMicrotaskEmpty.subscribe({
            next: () => {
                this._zone.run(() => {
                    this.tick();
                });
            }        
        }); 
}
登入後複製

在訂閱相關回呼函數中,this.tick()是不是很眼熟呢?如果你看了我之前的關於 Angular 生命週期函數的文章,那麼你肯定還會有印象,它是觸發視圖更新的關鍵呼叫。雖然在那篇生命週期介紹的文章中有講過這個函數,但本文的重點是變更檢測因此函數雖然相同但重點略有變化。 this.tick相關呼叫順序大概是這樣:

this.tick() ->
view.detectChanges() -> 
renderComponentOrTemplate() ->
refreshView()
登入後複製

這裡refreshView比較重要單獨拿出來分析一下:

function refreshView(tView, lView, templateFn, context) {
    ......
    if (templateFn !== null) {
        // 关键代码1
        executeTemplate(tView, lView, templateFn, ...);  }
    ......
    if (components !== null) {
        // 关键代码2
        refreshChildComponents(lView, components);
    }
}
登入後複製

這個過程中refreshView函數會被呼叫二次,第一次進入的是關鍵程式碼2分支,然後依序呼叫如下函數重新進入refreshView函數:

refreshChildComponents() ->
refreshChildComponents() ->
refreshComponent() ->
refreshView()
登入後複製

第二次進入refreshView函數呼叫的便是關鍵程式碼1分支了,即執行的是:executeTemplate函數。而該函數最終執行的是模板編譯產物中的AppComponent_Template函數:

function AppComponent_Template(rf, ctx) {
    if (rf & 1) {
        // 条件分支1
        i0["ɵɵelementStart"](0, "div", 0);
        i0["ɵɵlistener"]("click", function AppComponent_Template_div_click_0_listener() {
            return ctx.handleClick();
        });
        i0["ɵɵtext"](1);
        i0["ɵɵelementEnd"]();
    }
    if (rf & 2) {
        // 条件分支2
        i0["ɵɵadvance"](1);
        i0["ɵɵtextInterpolate"](ctx.title);
    }
}
登入後複製

如果還有讀者不清楚上述模板編譯產物中的函數是怎麼來的,建議閱讀之前關於依賴注入原理講解的文章,因篇幅限制不再贅述。此時AppComponent_Template函數執行的是條件分支2裡的程式碼,ɵɵadvance函數作用是更新相關的索引值,以確保找到正確的元素。這裡重點講講ɵɵtextInterpolate函數,它最終呼叫的是函數ɵɵtextInterpolate1:

function ɵɵtextInterpolate1(prefix, v0, suffix) {
    const lView = getLView();
    // 关键代码1
    const interpolated = interpolation1(lView, prefix, v0, suffix);
    if (interpolated !== NO_CHANGE) {
        // 关键代码2
        textBindingInternal(lView, getSelectedIndex(), interpolated);
    }
    return ɵɵtextInterpolate1;
}
登入後複製

值得指出的是,该函数名末尾是数字1,这是因为还有类似的ɵɵtextInterpolate2ɵɵtextInterpolate3等等,Angular 内部根据插值表达式的数量调用不同的专用函数,本文示例中文本节点的插值表达式数量为1,因此实际调用的是ɵɵtextInterpolate1函数。该函数主要做了两件事,关键代码1作用是比较插值表达式值有没有更新,关键代码2则是更新文本节点的值。先来看看关键代码1的函数interpolation1,它最终调用的是:

function bindingUpdated(lView, bindingIndex, value) {
    const oldValue = lView[bindingIndex];
    if (Object.is(oldValue, value)) {
        return false;
    }
    else {
        lView[bindingIndex] = value;
        return true;
    }
}
登入後複製

变更检测前的文本节点值称之为oldValue, 该值存储在lView中,lView我在之前的文章中也提到过,忘记了的读者可以去看看lView的作用。bindingUpdated首先会比较新值和旧值,比较的方法便是Object.is。如果新值旧值没有变化,则返回false。如果有变化,则更新lView中存储的值,并返回true。关键代码2的函数textBindingInternal最终调用的是下述函数:

function updateTextNode(renderer, rNode, value) {
    ngDevMode && ngDevMode.rendererSetText++;
    isProceduralRenderer(renderer) ? renderer.setValue(rNode, value) : rNode.textContent = value;
}
登入後複製

走完上述流程,我们点击div元素时,界面显示内容便会由aa变为bb,即完成了从应用数据的变更到 UI 状态的同步更新,这便是 Angular 最基本的变更检测过程了。

因篇幅限制,本文所举示例比较简单,但 Angular 的变更检测还有很多没有讲到。比如,如果应用是由若干个组件组成的,父子组件间的变更检测如何进行,以及如何通过策略优化变更检测等等。如果有对这方面感兴趣的朋友,欢迎关注我的个人公众号【朱玉洁的博客】,后续将在那里分享更多前端知识。

更多编程相关知识,请访问:编程学习!!

以上是透過範例入手聊聊Angular中的變更檢測的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!