ホームページ > 記事 > ウェブフロントエンド > Angular DOM 更新メカニズムの詳細な説明
この記事では、Angular DOM の更新メカニズムを理解します。一定の参考値があるので、困っている友達が参考になれば幸いです。
[関連チュートリアルの推奨事項: 「angular チュートリアル」]
モデルの変更によってトリガーされる DOM 更新は、すべてのフロントエンドにとって重要ですフレームワーク関数 (注: モデルとビューの同期を維持する)、もちろん Angular も例外ではありません。次のようにテンプレート式を定義します:
<span>Hello {{name}}</span>
または次のようなプロパティ バインディング (注: これは上記のコードと同等です):
<span [textContent]="'Hello ' + name"></span>
When each time the name
値 変更が発生すると、Angular は魔法のように DOM 要素を自動的に更新します (注: 一番上のコードは DOM テキスト ノード を更新し、上のコードは ## を更新します) #DOM 要素ノード 、2 つは異なります。後述します)。表面的には単純に見えますが、内部の仕組みは非常に複雑です。さらに、DOM 更新は Angular の 変更検出メカニズム の一部にすぎません。変更検出メカニズムは主に次の 3 つのステップで構成されます:
バインディングの更新
Angularの動的コンポーネントについて知っておくべきことは次のとおりです Angularコンパイラについて知るプログラムで使用されるコンポーネントをファクトリ クラスにコンパイルします。たとえば、次のコードは、Angular がファクトリ クラスからコンポーネントを作成する方法を示しています (注: ここでの作成者のロジックは少し混乱しているようです。前の文で言及した Angular コンパイラによってコンパイルされたファクトリ クラスは、実際にはコンパイラによって実行されるものであり、物事は 自動化されたものです ; 次のコードは、開発者が ComponentFactory## を通じて コンポーネント インスタンスを 手動 で作成する方法について説明しています。 #. つまり、彼はコンポーネントがどのようにインスタンス化されるかを言いたいのです): const factory = r.resolveComponentFactory(AComponent);
componentRef: ComponentRef<acomponent> = factory.create(injector);</acomponent>
Angular はこのファクトリ クラスを使用して
View Definition をインスタンス化し、その後 ## を使用します#viewDef ビューを作成する関数。 Angular は内部的にプログラムをビュー ツリーとみなします。プログラムには多くのコンポーネントがありますが、コンポーネントによって生成されるビュー構造を定義するための共通のビュー定義インターフェイスがあります (注: ViewDefinition Interface )、もちろん、Angular は各コンポーネント オブジェクトを使用して対応するビューを作成し、複数のビューからビュー ツリーを形成します。 (注: ここでの主な概念の 1 つは View であり、その構造は ViewDefinition Interface です)
组件工厂大部分代码是由编译器生成的不同视图节点组成的,这些视图节点是通过模板解析生成的(注:编译器生成的组件工厂是一个返回值为函数的函数,上文的 ComponentFactory 是 Angular 提供的类,供手动调用。当然,两者指向同一个事物,只是表现形式不同而已)。假设定义一个组件的模板如下:
<span>I am {{name}}</span>
编译器会解析这个模板生成包含如下类似的组件工厂代码(注:这只是最重要的部分代码):
function View_AComponent_0(l) { return jit_viewDef1(0, [ jit_elementDef2(0,null,null,1,'span',...), jit_textDef3(null,['I am ',...]) ], null, function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,1,0,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,0,null,null,1,'span',[],null,null,null,null,null)), (_l()(),jit_textDef_3(1,null,['I am ',''])) ], null, function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,1,0,currVal_0); }); } return {RenderType_AppComponent:RenderType_AppComponent,View_AppComponent_0:View_AppComponent_0};})
上面代码描述了视图的结构,并在实例化组件时会被调用。jit_viewDef_1
其实就是 viewDef 函数,用来创建视图(注:viewDef
函数很重要,因为视图是调用它创建的,生成的视图结构即是 ViewDefinition)。
viewDef 函数的第二个参数 nodes 有些类似 html 中节点的意思,但却不仅仅如此。上面代码中第二个参数是一个数组,其第一个数组元素 jit_elementDef_2
是元素节点定义,第二个数组元素 jit_textDef_3
是文本节点定义。Angular 编译器会生成很多不同的节点定义,节点类型是由 NodeFlags 设置的。稍后我们将看到 Angular 如何根据不同节点类型来做 DOM 更新。
本文只对元素和文本节点感兴趣:
export const enum NodeFlags { TypeElement = 1 << 0, TypeText = 1 << 1
让我们简要撸一遍。
注:上文作者说了一大段,其实核心就是,程序是一堆视图组成的,而每一个视图又是由不同类型节点组成的。而本文只关心元素节点和文本节点,至于还有个重要的指令节点在另一篇文章。
元素节点结构 是 Angular 编译每一个 html 元素生成的节点结构,它也是用来生成组件的,如对这点感兴趣可查看 Here is why you will not find components inside Angular。元素节点也可以包含其他元素节点和文本节点作为子节点,子节点数量是由 childCount
设置的。
所有元素定义是由 elementRef 函数生成的,而工厂函数中的 jit_elementDef_2()
就是这个函数。elementRef()
主要有以下几个一般性参数:
Name | Description |
---|---|
childCount | specifies how many children the current element have |
namespaceAndName | the name of the html element(注:如 'span') |
fixedAttrs | attributes defined on the element |
还有其他的几个具有特定性能的参数:
Name | Description |
---|---|
matchedQueriesDsl | used when querying child nodes |
ngContentIndex | used for node projection |
bindings | used for dom and bound properties update |
outputs, handleEvent | used for event propagation |
本文主要对 bindings 感兴趣。
注:从上文知道视图(view)是由不同类型节点(nodes)组成的,而元素节点(element nodes)是由 elementRef 函数生成的,元素节点的结构是由 ElementDef 定义的。
文本节点结构 是 Angular 编译每一个 html 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注:<span>I am {{name}}</span>
,span
是元素节点,I am {{name}}
是文本节点,也是 span
的子节点)。这个文本节点是由 textDef 函数生成的。它的第二个参数以字符串数组形式传进来(注: Angular v5.* 是第三个参数)。例如,下面的文本:
<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 操作类型:
Name | Construction in template |
---|---|
TypeElementAttribute | attr.name |
TypeElementClass | class.name |
TypeElementStyle | style.name |
元素和文本定义根据这些编译器可识别的绑定标志位,内部创建这些绑定依赖。每一种节点类型都有着不同的绑定生成逻辑(注:意思是 Angular 会根据 BindingFlags 来生成对应的 BindingDef)。
最让我们感兴趣的是 jit_viewDef_1
中最后那个参数:
function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,1,0,currVal_0); });
这个函数叫做 updateRenderer。它接收两个参数:_ck
和 _v
。_ck
是 check
的简写,其实就是 prodCheckAndUpdateNode 函数,而 _v
就是当前视图对象。updateRenderer
函数会在 每一次变更检测时 被调用,其参数 _ck
和 _v
也是这时被传入。
updateRenderer
函数逻辑主要是,从组件对象的绑定属性获取当前值,并调用 _ck
函数,同时传入视图对象、视图节点索引和绑定属性当前值。重要一点是 Angular 会为每一个视图执行 DOM 更新操作,所以必须传入视图节点索引参数(注:这个很好理解,上文说了 Angular 会依次对每一个 view 做模型视图同步过程)。你可以清晰看到 _ck
参数列表:
function prodCheckAndUpdateNode( view: ViewData, nodeIndex: number, argStyle: ArgumentType, v0?: any, v1?: any, v2?: any,
nodeIndex
是视图节点的索引,如果你模板中有多个表达式:
<h1>Hello {{name}}</h1> <h1>Hello {{age}}</h1>
编译器生成的 updateRenderer
函数如下:
var _co = _v.component; // here node index is 1 and property is `name` var currVal_0 = _co.name; _ck(_v,1,0,currVal_0); // here node index is 4 and bound property is `age` var currVal_1 = _co.age; _ck(_v,4,0,currVal_1);
现在我们已经知道 Angular 编译器生成的所有对象(注:已经有了 view,element node,text node 和 updateRenderer 这几个道具),现在我们可以探索如何使用这些对象来更新 DOM。
从上文我们知道变更检测期间 updateRenderer
函数传入的一个参数是 _ck
函数,而这个函数就是 prodCheckAndUpdateNode。这个函数在继续执行后,最终会调用 checkAndUpdateNodeInline ,如果绑定属性的数量超过 10,Angular 还提供了 checkAndUpdateNodeDynamic 这个函数(注:两个函数本质一样)。
checkAndUpdateNodeInline
函数会根据不同视图节点类型来执行对应的检查更新函数:
case NodeFlags.TypeElement -> checkAndUpdateElementInline case NodeFlags.TypeText -> checkAndUpdateTextInline case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline
让我们看下这些函数是做什么的,至于 NodeFlags.TypeDirective
可以查看我写的文章 The mechanics of property bindings update in Angular 。
注:因为本文只关注
element node 和 text node
。
对于元素节点,会调用函数 checkAndUpdateElementInline 以及 checkAndUpdateElementValue,checkAndUpdateElementValue
函数会检查绑定形式是否是 [attr.name, class.name, style.some]
或是属性绑定形式:
case BindingFlags.TypeElementAttribute -> setElementAttribute case BindingFlags.TypeElementClass -> setElementClass case BindingFlags.TypeElementStyle -> setElementStyle case BindingFlags.TypeProperty -> setElementProperty;
然后使用渲染器对应的方法来对该节点执行对应操作,比如使用 setElementClass
给当前节点 span
添加一个 class
。
对于文本节点类型,会调用 checkAndUpdateTextInline ,下面是主要部分:
if (checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) { value = text + _addInterpolationPart(...); view.renderer.setValue(DOMNode, value); }
它会拿到 updateRenderer
函数传过来的当前值(注:即上文的 _ck(_v,4,0,currVal_1);
),与上一次变更检测时的值相比较。视图数据包含有 oldValues 属性,如果属性值如 name
发生变化,Angular 会使用最新 name
值合成最新的字符串文本,如 Hello New World
,然后使用渲染器更新 DOM 上对应的文本。
注:更新元素节点和文本节点都提到了渲染器(renderer),这也是一个重要的概念。每一个视图对象都有一个 renderer 属性,即是 Renderer2 的引用,也就是组件渲染器,DOM 的实际更新操作由它完成。因为 Angular 是跨平台的,这个 Renderer2 是个接口,这样根据不同 Platform 就选择不同的 Renderer。比如,在浏览器里这个 Renderer 就是 DOMRenderer,在服务端就是 ServerRenderer,等等。从这里可看出,Angular 框架设计做了很好的抽象。
我知道有大量难懂的信息需要消化,但是只要理解了这些知识,你就可以更好的设计程序或者去调试 DOM 更新相关的问题。我建议你按照本文提到的源码逻辑,使用调试器或 debugger 语句
一步步去调试源码。
英文原文地址:https://blog.angularindepth.com/the-mechanics-of-dom-updates-in-angular-3b2970d5c03d
更多编程相关知识,请访问:编程教学!!
以上がAngular DOM 更新メカニズムの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。