> 웹 프론트엔드 > JS 튜토리얼 > Angular DOM 업데이트 메커니즘에 대한 자세한 설명

Angular DOM 업데이트 메커니즘에 대한 자세한 설명

青灯夜游
풀어 주다: 2021-05-27 11:11:07
앞으로
1823명이 탐색했습니다.

이 글에서는 Angular DOM의 업데이트 메커니즘을 안내합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.

Angular DOM 업데이트 메커니즘에 대한 자세한 설명

【관련 튜토리얼 추천: "angular Tutorial"】

모델 변경으로 인해 발생하는 DOM 업데이트는 모든 프런트엔드 프레임워크의 중요한 기능입니다(참고: 모델과 뷰를 동기화 상태로 유지). 각도도 예외는 아닙니다. 다음과 같이 템플릿 표현식을 정의합니다:

<span>Hello {{name}}</span>
로그인 후 복사

또는 다음과 유사한 속성 바인딩(참고: 위 코드와 동일합니다):

<span [textContent]="&#39;Hello &#39; + name"></span>
로그인 후 복사

name 값이 변경될 때마다 Angular는 마술처럼 평소와 같이 DOM 요소를 자동으로 업데이트합니다(참고: 상위 코드는 name 值发生变化时,Angular 会神奇般的自动更新 DOM 元素(注:最上面代码是更新 DOM 文本节点,上面代码是更新 DOM 元素节点,两者是不一样的,下文解释)。这表面上看起来很简单,但是其内部工作相当复杂。而且,DOM 更新仅仅是 Angular 变更检测机制 的一部分,变更检测机制主要由以下三步组成:

, 둘은 서로 다릅니다. 아래에 설명되어 있습니다. 표면적으로는 단순해 보이지만 내부 동작은 상당히 복잡합니다. 그리고 DOM 업데이트는 단지 Angular입니다.

변경 감지 메커니즘

의 일부인 변경 감지 메커니즘은 주로 다음 세 단계로 구성됩니다.

    DOM 업데이트(참고: 이 문서에서 설명할 항목)

하위 구성 요소 <코드 >입력 바인딩 업데이트

쿼리 목록 업데이트

이 문서에서는 주로 변경 감지 메커니즘의 렌더링 부분(즉, DOM 업데이트 부분)을 살펴봅니다. 이전에 이 문제에 대해 궁금하셨다면 계속 읽으시면 확실히 이해가 되실 것입니다.

해당 소스코드를 인용할 때, 프로그램이 프로덕션 모드에서 실행되고 있다고 가정합니다. 시작해 봅시다! 프로그램 내부 구조DOM 업데이트를 살펴보기 전에 먼저 Angular 프로그램이 내부적으로 어떻게 설계되었는지 살펴보겠습니다. View내 기사에서

다음은 Angular의 동적 구성 요소에 대해 알아야 할 사항입니다. Angular 컴파일러는 프로그램에 사용되는 구성 요소를 팩토리 클래스(팩토리)로 컴파일한다는 점을 알아두세요. 예를 들어 다음 코드는 Angular가 팩토리 클래스에서 구성 요소를 생성하는 방법을 보여줍니다. 개발자가 무엇이든 하도록 요구합니다. 다음 코드는 개발자가 ComponentFactory를 통해 Component 인스턴스를 수동으로 생성하는 방법에 대해 설명합니다.

const factory = r.resolveComponentFactory(AComponent);
componentRef: ComponentRef<AComponent> = factory.create(injector);
로그인 후 복사
. Angular는 이 팩토리 클래스를 사용하여 View Definition을 인스턴스화한 다음 viewDef 함수를 사용하여 뷰를 생성합니다. Angular는 내부적으로 프로그램을 뷰 트리로 간주합니다. 프로그램에는 많은 구성 요소가 있지만 구성 요소에 의해 생성된 뷰 구조를 정의하는 공통 뷰 정의 인터페이스가 있습니다(참고: 즉

ViewDefinition 인터페이스🎜🎜). 각 구성 요소 개체는 해당 뷰를 생성하여 여러 뷰에서 뷰 트리를 형성합니다. (참고: 여기서 주요 개념 중 하나는 🎜View🎜이며 구조는 🎜🎜ViewDefinition Interface🎜🎜)🎜

组件工厂

组件工厂大部分代码是由编译器生成的不同视图节点组成的,这些视图节点是通过模板解析生成的(注:编译器生成的组件工厂是一个返回值为函数的函数,上文的 ComponentFactory 是 Angular 提供的类,供手动调用。当然,两者指向同一个事物,只是表现形式不同而已)。假设定义一个组件的模板如下:

&lt;span&gt;I am {{name}}&lt;/span&gt;
로그인 후 복사

编译器会解析这个模板生成包含如下类似的组件工厂代码(注:这只是最重要的部分代码):

function View_AComponent_0(l) {
    return jit_viewDef1(0,
        [
          jit_elementDef2(0,null,null,1,&#39;span&#39;,...),
          jit_textDef3(null,[&#39;I am &#39;,...])
        ], 
        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 = [&#39;&#39;];
     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,&#39;span&#39;,[],null,null,null,null,null)),
                (_l()(),jit_textDef_3(1,null,[&#39;I am &#39;,&#39;&#39;]))
            ],
            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() 主要有以下几个一般性参数:

NameDescription
childCountspecifies how many children the current element have
namespaceAndNamethe name of the html element(注:如 'span')
fixedAttrsattributes defined on the element

还有其他的几个具有特定性能的参数:

NameDescription
matchedQueriesDslused when querying child nodes
ngContentIndexused for node projection
bindingsused for dom and bound properties update
outputs, handleEventused for event propagation

本文主要对 bindings 感兴趣。

注:从上文知道视图(view)是由不同类型节点(nodes)组成的,而元素节点(element nodes)是由 elementRef 函数生成的,元素节点的结构是由 ElementDef 定义的。

文本节点的结构定义

文本节点结构 是 Angular 编译每一个 html 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注:&lt;span&gt;I am {{name}}&lt;/span&gt;span 是元素节点,I am {{name}} 是文本节点,也是 span 的子节点)。这个文本节点是由 textDef 函数生成的。它的第二个参数以字符串数组形式传进来(注: Angular v5.* 是第三个参数)。例如,下面的文本:

<h1>Hello {{name}} and another {{prop}}</h1>
로그인 후 복사

将要被解析为一个数组:

["Hello ", " and another ", ""]
로그인 후 복사

然后被用来生成正确的绑定:

{
  text: &#39;Hello&#39;,
  bindings: [
    {
      name: &#39;name&#39;,
      suffix: &#39; and another &#39;
    },
    {
      name: &#39;prop&#39;,
      suffix: &#39;&#39;
    }
  ]
}
로그인 후 복사

在脏检查(注:即变更检测)阶段会这么用来生成文本:

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 操作类型:

NameConstruction in template
TypeElementAttributeattr.name
TypeElementClassclass.name
TypeElementStylestyle.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_ckcheck 的简写,其实就是 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);
로그인 후 복사

更新 DOM

现在我们已经知道 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 以及 checkAndUpdateElementValuecheckAndUpdateElementValue 函数会检查绑定形式是否是 [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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:juejin.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿