作为“为大型前端项目”而设计的前端框架,Angular 其实有许多值得参考和学习的设计,本系列主要用于研究这些设计和功能的实现原理。本文围绕 Angular 的核心功能 Ivy 编译器,介绍其中变更检测的过程。
上一篇《Angular框架解读–Ivy编译器之增量DOM》 中,我介绍了 Ivy 编译器中使用了增量 DOM 的设计。在 Ivy 中,通过编译器将模板编译为template
渲染函数,该过程会将对模板的解析编译成增量 DOM 相关的指令。其中,在elementStart()
执行时,我们可以看到会通过createElementNode()
方法来创建 DOM。
而增量 DOM 中的变更检测、Diff 和更新 DOM 等能力,都与elementStart()
方法紧紧关联着。
Ivy 中的变更检测 ngZone 的自动变更检测 在《Angular框架解读–Zone区域之ngZone》 一文中,我们介绍了默认情况下,所有异步操作都在 Angular Zone 内。该逻辑在创建 Angular 应用的时候便已添加,这会自动触发变更检测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Injectable ()export class ApplicationRef { ... constructor ( private _zone: NgZone, private _injector: Injector, private _exceptionHandler: ErrorHandler, private _componentFactoryResolver: ComponentFactoryResolver, private _initStatus: ApplicationInitStatus ) { this ._onMicrotaskEmptySubscription = this ._zone .onMicrotaskEmpty .subscribe ({ next : () => { this ._zone .run (() => { this .tick (); }); } }); } }
tick
方法中,核心的逻辑是调用了view.detectChanges()
来检测更新。该接口来自ChangeDetectorRef
,它提供变更检测功能的基类。
变更检测树收集所有要检查变更的视图,可以使用方法从树中添加和删除视图,启动更改检测,并将视图显式标记为_dirty_
,这意味着它们已更改并需要重新渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export abstract class ChangeDetectorRef { abstract markForCheck (): void ; abstract detach (): void ; abstract detectChanges (): void ; abstract checkNoChanges (): void ; abstract reattach (): void ; }
在上述的ChangeDetectorRef
中,变更检测detectChanges()
中,核心逻辑调用了refreshView()
。
refreshView 视图更新处理 refreshView()
用于在更新模式下处理视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 export function refreshView<T>( tView : TView , lView : LView , templateFn : ComponentTemplate <{}>|null , context : T) { ngDevMode && assertEqual (isCreationMode (lView), false , 'Should be run in update mode' ); const flags = lView[FLAGS ]; enterView (lView); const isInCheckNoChangesPass = isInCheckNoChangesMode (); try { resetPreOrderHookFlags (lView); setBindingIndex (tView.bindingStartIndex ); if (templateFn !== null ) { executeTemplate (tView, lView, templateFn, RenderFlags .Update , context); } if (!isInCheckNoChangesPass) { if (hooksInitPhaseCompleted) { executeCheckHooks (lView, preOrderCheckHooks, null ); } else { executeInitAndCheckHooks (lView, preOrderHooks, InitPhaseState .OnInitHooksToBeRun , null ); incrementInitPhaseFlags (lView, InitPhaseState .OnInitHooksToBeRun ); } } markTransplantedViewsForRefresh (lView); refreshEmbeddedViews (lView); if (tView.contentQueries !== null ) { refreshContentQueries (tView, lView); } if (!isInCheckNoChangesPass) { if (hooksInitPhaseCompleted) { executeCheckHooks (lView, contentCheckHooks); } else { executeInitAndCheckHooks (lView, contentHooks, InitPhaseState .AfterContentInitHooksToBeRun ); incrementInitPhaseFlags (lView, InitPhaseState .AfterContentInitHooksToBeRun ); } } processHostBindingOpCodes (tView, lView); const components = tView.components ; if (components !== null ) { refreshChildComponents (lView, components); } const viewQuery = tView.viewQuery ; if (viewQuery !== null ) { executeViewQueryFn (RenderFlags .Update , viewQuery, context); } if (!isInCheckNoChangesPass) { if (hooksInitPhaseCompleted) { executeCheckHooks (lView, viewCheckHooks); } else { executeInitAndCheckHooks (lView, viewHooks, InitPhaseState .AfterViewInitHooksToBeRun ); incrementInitPhaseFlags (lView, InitPhaseState .AfterViewInitHooksToBeRun ); } } if (tView.firstUpdatePass === true ) { tView.firstUpdatePass = false ; } if (!isInCheckNoChangesPass) { lView[FLAGS ] &= ~(LViewFlags .Dirty | LViewFlags .FirstLViewPass ); } if (lView[FLAGS ] & LViewFlags .RefreshTransplantedView ) { lView[FLAGS ] &= ~LViewFlags .RefreshTransplantedView ; updateTransplantedViewCount (lView[PARENT ] as LContainer , -1 ); } } finally { leaveView (); } }
可以看到,refreshView()
的处理包括按特定顺序执行的多个步骤:
在更新模式下,执行template
模板函数。
执行钩子。
刷新 Query 查询。
设置 host 绑定。
刷新子(嵌入式和组件)视图。
除此之外,在变更检测的最开始执行了enterView()
,此时 Angular 会用新的LView
交换当前的LView
。这样的处理主要出于性能原因,通过将LView
存储在模块的顶层,最大限度地减少了要读取的属性数量。
LView
用于存储从模板调用指令时处理指令所需的所有信息,在《Angular框架解读–Ivy编译器的视图数据和依赖解析》 中有介绍。
每个嵌入视图和组件视图都有自己的LView
。在处理特定视图时,我们将viewData
设置为该LView
。当该视图完成处理后,viewData
被设置回原始viewData
之前的任何内容(父LView
)。
在refreshView()
处理中,每当进入新视图时会存储LView
以备后用。我们也可以看到当退出视图时,通过执行leaveView()
离开当前的LView
,恢复原来的状态。
以上便是变更检测过程中的视图处理逻辑。
创建与更新视图的处理 我们可以对比下创建视图的过程,处理视图创建的过程在renderView()
中实现。
renderView()
用于在创建模式下处理视图,该过程包括按特定顺序执行的多个步骤:
创建视图查询函数(如果有)。
在创建模式下,执行template()
模板函数。
更新静态 Query 查询(如果有)。
创建在给定视图中定义的子组件。
在上一篇文章中,我们介绍了这样一个组件:
1 2 3 4 5 6 7 8 9 import { Component , Input } from "@angular/core" ;@Component ({ selector : "greet" , template : "<div> Hello, {{name}}! </div>" , }) export class GreetComponent { @Input () name : string ; }
经ngtsc
编译后,产物会大概长这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GreetComponent .ɵcmp = i0.ɵɵdefineComponent ({ type : GreetComponent , tag : "greet" , factory : () => new GreetComponent (), template : function (rf, ctx ) { if (rf & RenderFlags .Create ) { i0.ɵɵelementStart (0 , "div" ); i0.ɵɵtext (1 ); i0.ɵɵelementEnd (); } if (rf & RenderFlags .Update ) { i0.ɵɵadvance (1 ); i0.ɵɵtextInterpolate1 ("Hello " , ctx.name , "!" ); } }, });
可以看到,创建模式下的模板函数逻辑,与更新视图模式下的模板函数逻辑是有区别的。在创建模式下,elementStart
、elementEnd
我们在上一篇文章中有详细地介绍了。而在更新模式下,textInterpolate1
表示当文本节点有 1 个内插值时,使用由其他文本包围的单个绑定值更新文本内容:
1 2 3 4 5 export function interpolation1 (lView: LView, prefix: string , v0: any , suffix: string ): string | NO_CHANGE { const different = bindingUpdated (lView, nextBindingIndex (), v0); return different ? prefix + renderStringify (v0) + suffix : NO_CHANGE ; }
可以见到,在具体的模板函数指令中,会自行进行变更的检查,如果有发生了变化,则进行更新。bindingUpdated()
方法会在需要更改时更新绑定,然后返回是否已更新。
而对于视图更新时,除了textInterpolate1
这种比较简单的场景下的模板更新,子组件通过refreshComponent
来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function refreshComponent (hostLView: LView, componentHostIdx: number ): void { ngDevMode && assertEqual (isCreationMode (hostLView), false , 'Should be run in update mode' ); const componentView = getComponentLViewByIndex (componentHostIdx, hostLView); if (viewAttachedToChangeDetector (componentView)) { const tView = componentView[TVIEW ]; if (componentView[FLAGS ] & (LViewFlags .CheckAlways | LViewFlags .Dirty )) { refreshView (tView, componentView, tView.template , componentView[CONTEXT ]); } else if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH ] > 0 ) { refreshContainsDirtyView (componentView); } } }
同样的,在处理子组件的时候,需要检查子组件是否被标记为 CheckAlways 或者 Dirty,才进入组件视图并处理其绑定、查询等来刷新组件。
结束语 以上,便是 Angular Ivy 中的变更检测了。
可以看到,在 Angular 中将被标记为 CheckAlways 或者 Dirty 的组件进行视图刷新,在每个变更周期中,会执行template()
模板函数中的更新模式下逻辑。而在template()
模板函数中的具体指令逻辑中,还会根据原来的值和新的值进行比较,有差异的时候才会进行更新。
参考
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场 边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢