[译] Angular DOM 更新机制
原文链接: The mechanics of DOM updates in Angular
由模型变化触发的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 view 的同步),当然 Angular 也不例外。定义一个如下模板表达式: <span>Hello {{name}}</span> 或者类似下面的属性绑定(注:这与上面代码等价): <span [textContent]="'Hello ' + name"></span> 当每次
本文主要探索变更检测机制的渲染部分(即 DOM updates 部分)。如果你之前也对这个问题很好奇,可以继续读下去,绝对让你茅塞顿开。 在引用相关源码时,假设程序是以生产模式运行。让我们开始吧! 程序内部架构在探索 DOM 更新之前,我们先搞清楚 Angular 程序内部究竟是如何设计的,简单回顾下吧。 视图从我的这篇文章 Here is what you need to know about dynamic components inAngular 知道 Angular 编译器会把程序中使用的组件编译为一个工厂类(factory)。例如,下面代码展示 Angular 如何从工厂类中创建一个组件(注:这里作者逻辑貌似有点乱,前一句说的 Angular 编译器编译的工厂类,其实是编译器去做的,不需要开发者做任何事情,是自动化的事情;而下面代码说的是开发者如何手动通过 ComponentFactory 来创建一个 Component 实例。总之,他是想说组件是怎么被实例化的): const factory = r.resolveComponentFactory(AComponent); componentRef: ComponentRef<AComponent> = factory.create(injector); Angular 使用这个工厂类来实例化 View Definition ,然后使用 viewDef 函数来 创建视图。Angular 内部把一个程序看作为一颗视图树,一个程序虽然有众多组件,但有一个公共的视图定义接口来定义由组件生成的视图结构(注:即 ViewDefinition Interface),当然 Angular 使用每一个组件对象来创建对应的视图,从而由多个视图组成视图树。(注:这里有一个主要概念就是视图,其结构就是 ViewDefinition Interface) 组件工厂组件工厂大部分代码是由编译器生成的不同视图节点组成的,这些视图节点是通过模板解析生成的(注:编译器生成的组件工厂是一个返回值为函数的函数,上文的 ComponentFactory 是 Angular 提供的类,供手动调用。当然,两者指向同一个事物,只是表现形式不同而已)。假设定义一个组件的模板如下: <span>I am {{name}}</span> 编译器会解析这个模板生成包含如下类似的组件工厂代码(注:这只是最重要的部分代码): function View_AComponent_0(l) { return jit_viewDef1(0,[ jit_elementDef2(0,null,1,'span',...),jit_textDef3(null,['I am ',...]) ],function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,currVal_0); 注:由 AppComponent 组件编译生成的工厂函数完整代码如下 (function(jit_createRendererType2_0,jit_viewDef_1,jit_elementDef_2,jit_textDef_3) { var styles_AppComponent = ['']; var RenderType_AppComponent = jit_createRendererType2_0({encapsulation:0,styles:styles_AppComponent,data:{}}); function View_AppComponent_0(_l) { return jit_viewDef_1(0,[ (_l()(),jit_elementDef_2(0,[],null)),(_l()(),jit_textDef_3(1,''])) ],_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,currVal_0); }); } return {RenderType_AppComponent:RenderType_AppComponent,View_AppComponent_0:View_AppComponent_0};}) 上面代码描述了视图的结构,并在实例化组件时会被调用。 viewDef 函数的第二个参数 nodes 有些类似 html 中节点的意思,但却不仅仅如此。上面代码中第二个参数是一个数组,其第一个数组元素 本文只对元素和文本节点感兴趣: export const enum NodeFlags { TypeElement = 1 << 0,TypeText = 1 << 1 让我们简要撸一遍。 注:上文作者说了一大段,其实核心就是, 程序是一堆视图组成的,而每一个视图又是由不同类型节点组成的。而本文只关心元素节点和文本节点,至于还有个重要的指令节点在另一篇文章。 元素节点的结构定义元素节点结构 是 Angular 编译每一个 html 元素生成的节点结构,它也是用来生成组件的,如对这点感兴趣可查看 Here is why you will not find components insideAngular。元素节点也可以包含其他元素节点和文本节点作为子节点,子节点数量是由 所有元素定义是由 elementRef 函数生成的,而工厂函数中的
还有其他的几个具有特定性能的参数:
本文主要对 bindings 感兴趣。 注:从上文知道视图(view)是由不同类型节点(nodes)组成的,而元素节点(element nodes)是由 elementRef 函数生成的,元素节点的结构是由 ElementDef 定义的。 文本节点的结构定义文本节点结构 是 Angular 编译每一个 html 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注: <h1>Hello {{name}} and another {{prop}}</h1> 将要被解析为一个数组: ["Hello "," and another ",""] 然后被用来生成正确的绑定: { text: 'Hello',bindings: [ { name: 'name',suffix: ' and another ' },{ name: 'prop',suffix: '' } ] } 在脏检查(注:即变更检测)阶段会这么用来生成文本: text + context[bindings[0][property]] + context[bindings[0][suffix]] + context[bindings[1][property]] + context[bindings[1][suffix]] 注:同上,文本节点是由 textDef 函数生成的,结构是由 TextDef 定义的。既然已经知道了两个节点的定义和生成,那节点上的属性绑定, Angular 是怎么处理的呢? 节点的绑定Angular 使用 BindingDef 来定义每一个节点的绑定依赖,而这些绑定依赖通常是组件类的属性。在变更检测时 Angular 会根据这些绑定来决定如何更新节点和提供上下文信息。具体哪一种操作是由 BindingFlags 决定的,下面列表展示了具体的 DOM 操作类型:
元素和文本定义根据这些编译器可识别的绑定标志位,内部创建这些绑定依赖。每一种节点类型都有着不同的绑定生成逻辑(注:意思是 Angular 会根据 BindingFlags 来生成对应的 BindingDef)。 更新渲染器最让我们感兴趣的是 function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,currVal_0); }); 这个函数叫做 updateRenderer。它接收两个参数:
function prodCheckAndUpdateNode( view: ViewData,nodeIndex: number,argStyle: ArgumentType,v0?: any,v1?: any,v2?: any,
<h1>Hello {{name}}</h1> <h1>Hello {{age}}</h1> 编译器生成的 var _co = _v.component; // here node index is 1 and property is `name` var currVal_0 = _co.name; _ck(_v,currVal_0); // here node index is 4 and bound property is `age` var currVal_1 = _co.age; _ck(_v,4,currVal_1); 更新 DOM现在我们已经知道 Angular 编译器生成的所有对象(注:已经有了 view,element node,text node 和 updateRenderer 这几个道具),现在我们可以探索如何使用这些对象来更新 DOM。 从上文我们知道变更检测期间
case NodeFlags.TypeElement -> checkAndUpdateElementInline case NodeFlags.TypeText -> checkAndUpdateTextInline case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline 让我们看下这些函数是做什么的,至于
注:因为本文只关注
元素节点对于元素节点,会调用函数 checkAndUpdateElementInline 以及 checkAndUpdateElementValue, case BindingFlags.TypeElementAttribute -> setElementAttribute case BindingFlags.TypeElementClass -> setElementClass case BindingFlags.TypeElementStyle -> setElementStyle case BindingFlags.TypeProperty -> setElementProperty; 然后使用渲染器对应的方法来对该节点执行对应操作,比如使用 文本节点对于文本节点类型,会调用 checkAndUpdateTextInline ,下面是主要部分: if (checkAndUpdateBinding(view,nodeDef,bindingIndex,newValue)) { value = text + _addInterpolationPart(...); view.renderer.setValue(DOMNode,value); } 它会拿到 注:更新元素节点和文本节点都提到了渲染器(renderer),这也是一个重要的概念。每一个视图对象都有一个 renderer 属性,即是 Renderer2 的引用,也就是组件渲染器,DOM 的实际更新操作由它完成。因为 Angular 是跨平台的,这个 Renderer2 是个接口,这样根据不同 Platform 就选择不同的 Renderer。比如,在浏览器里这个 Renderer 就是 DOMRenderer,在服务端就是 ServerRenderer,等等。 从这里可看出,Angular 框架设计做了很好的抽象。 结论我知道有大量难懂的信息需要消化,但是只要理解了这些知识,你就可以更好的设计程序或者去调试 DOM 更新相关的问题。我建议你按照本文提到的源码逻辑,使用调试器或 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |