热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

详解AngularDOM的更新机制

本篇文章带大家了解一下AngularDOM的更新机制。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
本篇文章带大家了解一下Angular DOM的更新机制。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。

【相关教程推荐:《angular教程》】

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

Hello {{name}}

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

当每次 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 = factory.create(injector);

Angular 使用这个工厂类来实例化 View Definition ,然后使用 viewDef 函数来 创建视图。Angular 内部把一个程序看作为一颗视图树,一个程序虽然有众多组件,但有一个公共的视图定义接口来定义由组件生成的视图结构(注:即 ViewDefinition Interface),当然 Angular 使用每一个组件对象来创建对应的视图,从而由多个视图组成视图树。(注:这里有一个主要概念就是视图,其结构就是 ViewDefinition Interface

组件工厂

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

I am {{name}}

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

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(注:如 &#39;span&#39;)
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 文本 生成的节点结构。通常它是元素定义节点的子节点,就像我们本文的示例那样(注:I am {{name}}span 是元素节点,I am {{name}} 是文本节点,也是 span 的子节点)。这个文本节点是由 textDef 函数生成的。它的第二个参数以字符串数组形式传进来(注: Angular v5.* 是第三个参数)。例如,下面的文本:

Hello {{name}} and another {{prop}}

将要被解析为一个数组:

["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 是视图节点的索引,如果你模板中有多个表达式:

Hello {{name}}

Hello {{age}}

编译器生成的 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的更新机制的详细内容,更多请关注其它相关文章!


推荐阅读
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • Lodop中特殊符号打印设计和预览样式不同的问题解析
    本文主要解析了在Lodop中使用特殊符号打印设计和预览样式不同的问题。由于调用的本机ie引擎版本可能不同,导致在不同浏览器下样式解析不同。同时,未指定文字字体和样式设置也会导致打印设计和预览的差异。文章提出了通过指定具体字体和样式来解决问题的方法,并强调了以打印预览和虚拟打印机测试为准。 ... [详细]
  • Final关键字的含义及用法详解
    本文详细介绍了Java中final关键字的含义和用法。final关键字可以修饰非抽象类、非抽象类成员方法和变量。final类不能被继承,final类中的方法默认是final的。final方法不能被子类的方法覆盖,但可以被继承。final成员变量表示常量,只能被赋值一次,赋值后值不再改变。文章还讨论了final类和final方法的应用场景,以及使用final方法的两个原因:锁定方法防止修改和提高执行效率。 ... [详细]
  • 本文介绍了求解gcdexgcd斐蜀定理的迭代法和递归法,并解释了exgcd的概念和应用。exgcd是指对于不完全为0的非负整数a和b,gcd(a,b)表示a和b的最大公约数,必然存在整数对x和y,使得gcd(a,b)=ax+by。此外,本文还给出了相应的代码示例。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 电销机器人作为一种人工智能技术载体,可以帮助企业提升电销效率并节省人工成本。然而,电销机器人市场缺乏统一的市场准入标准,产品品质良莠不齐。创业者在代理或购买电销机器人时应注意谨防用录音冒充真人语音通话以及宣传技术与实际效果不符的情况。选择电销机器人时需要考察公司资质和产品品质,尤其要关注语音识别率。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 如何去除Win7快捷方式的箭头
    本文介绍了如何去除Win7快捷方式的箭头的方法,通过生成一个透明的ico图标并将其命名为Empty.ico,将图标复制到windows目录下,并导入注册表,即可去除箭头。这样做可以改善默认快捷方式的外观,提升桌面整洁度。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 安装mysqlclient失败解决办法
    本文介绍了在MAC系统中,使用django使用mysql数据库报错的解决办法。通过源码安装mysqlclient或将mysql_config添加到系统环境变量中,可以解决安装mysqlclient失败的问题。同时,还介绍了查看mysql安装路径和使配置文件生效的方法。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
author-avatar
mobiledu2502853033
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有