作为“为大型前端项目”而设计的前端框架,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
更欢迎来被删的前端游乐场 边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢