最新文章专题视频专题问答1问答10问答100问答1000问答2000关键字专题1关键字专题50关键字专题500关键字专题1500TAG最新视频文章推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37视频文章20视频文章30视频文章40视频文章50视频文章60 视频文章70视频文章80视频文章90视频文章100视频文章120视频文章140 视频2关键字专题关键字专题tag2tag3文章专题文章专题2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章专题3
当前位置: 首页 - 科技 - 知识百科 - 正文

[译]AngularDOM更新机制-Laravel/Angular技术分享

来源:懂视网 责编:小采 时间:2020-11-27 19:31:36
文档

[译]AngularDOM更新机制-Laravel/Angular技术分享

[译]AngularDOM更新机制-Laravel/Angular技术分享:本篇文章主要的向大家介绍了angularjs dom的更新机制,还有关于angularjs的模型表达式和程序内部架构等很多知识点,下面就让我们一起开始学习吧angularjs的模型表达式:由模型变化触发的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 vi
推荐度:
导读[译]AngularDOM更新机制-Laravel/Angular技术分享:本篇文章主要的向大家介绍了angularjs dom的更新机制,还有关于angularjs的模型表达式和程序内部架构等很多知识点,下面就让我们一起开始学习吧angularjs的模型表达式:由模型变化触发的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 vi

angularjs的模型表达式:

由模型变化触发的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 view 的同步),当然 Angular 也不例外。定义一个如下模板表达式:

<span>Hello {{name}}</span>

或者类似下面的属性绑定(注:这与上面代码等价):

<span [textContent]="'Hello ' + name"></span>

当每次 name 值发生变化时,Angular 会神奇般的自动更新 DOM 元素(注:最上面代码是更新 DOM 文本节点,上面代码是更新 DOM 元素节点,两者是不一样的,下文解释)。这表面上看起来很简单,但是其内部工作相当复杂。而且,DOM 更新仅仅是 Angular 变更检测机制 的一部分,变更检测机制主要由以下三步组成:

  • DOM updates(注:即本文将要解释的内容)

  • child components Input bindings updates

  • query list updates

  • 本文主要探索变更检测机制的渲染部分(即 DOM updates 部分)。如果你之前也对这个问题很好奇,可以继续读下去,绝对让你茅塞顿开。

    在引用相关源码时,假设程序是以生产模式运行。让我们开始吧!

    程序内部架构

    在探索 DOM 更新之前,我们先搞清楚 Angular 程序内部究竟是如何设计的,简单回顾下吧。

    视图

    从我的这篇文章 Here is what you need to know about dynamic components in Angular 知道 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,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() 主要有以下几个一般性参数:

    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 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注:<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 操作类型:

    NameConstruction in template
    TypeElementAttributeattr.name
    TypeElementClassclass.name
    TypeElementStylestyle.name

    元素和文本定义根据这些编译器可识别的绑定标志位,内部创建这些绑定依赖。每一种节点类型都有着不同的绑定生成逻辑(注:意思是 Angular 会根据 BindingFlags 来生成对应的 BindingDef)。(想看更多就到PHP中文网AngularJS开发手册中学习)

    更新渲染器

    最让我们感兴趣的是 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 以及 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 上对应的文本。(想看更多就到PHP中文网AngularJS开发手册中学习)

    注:更新元素节点和文本节点都提到了渲染器(renderer),这也是一个重要的概念。每一个视图对象都有一个 renderer 属性,即是 Renderer2 的引用,也就是组件渲染器,DOM 的实际更新操作由它完成。因为 Angular 是跨平台的,这个 Renderer2 是个接口,这样根据不同 Platform 就选择不同的 Renderer。比如,在浏览器里这个 Renderer 就是 DOMRenderer,在服务端就是 ServerRenderer,等等。从这里可看出,Angular 框架设计做了很好的抽象。

    结论

    我知道有大量难懂的信息需要消化,但是只要理解了这些知识,你就可以更好的设计程序或者去调试 DOM 更新相关的问题。我建议你按照本文提到的源码逻辑,使用调试器或 debugger 语句 一步步去调试源码。

    好了,本篇文章到这就结束了(想看更多就到PHP中文网AngularJS使用手册中学习),有问题的可以在下方留言提问。

    文档

    [译]AngularDOM更新机制-Laravel/Angular技术分享

    [译]AngularDOM更新机制-Laravel/Angular技术分享:本篇文章主要的向大家介绍了angularjs dom的更新机制,还有关于angularjs的模型表达式和程序内部架构等很多知识点,下面就让我们一起开始学习吧angularjs的模型表达式:由模型变化触发的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 vi
    推荐度:
    • 热门焦点

    最新推荐

    猜你喜欢

    热门推荐

    专题
    Top