属性绑定的机制在Angular中更新

所有现代框架都旨在帮助实现带有组件的UI组合。 自然地,您将具有父子层次结构,并且框架应提供一种在子与父之间进行通信的方法。 Angular提供了两种主要的通信方式-通过输入/输出绑定和服务。 我更喜欢将第一种方法用于无状态表示组件,而将DI用于有状态容器组件

本文涉及第一种方法。 特别是,它探索了当父绑定值更改时Angular如何更新子输入属性的基本机制。 它继续了一系列有关变更检测的文章,并以我之前的文章Angular中的DOM更新机制为基础 。 由于我们将研究Angular如何更新DOM元素和组件的输入属性,因此假设您知道Angular如何在内部表示指令和组件。 如果不这样做,请阅读以下内容继续熟悉该主题, 这就是为什么您无法在Angular中找到组件的原因 。 在整篇文章中,我将互换使用术语指令和组件,因为Angular在内部将组件表示为指令。

绑定模板语法

您可能知道Angular 为属性绑定提供了模板语法 -[]。 该语法是通用的,因此可以应用于子组件和本机DOM元素。 因此,如果您要将一些数据传递给子b-comp并从父A组件跨越,则可以在组件模板中执行以下操作:

import { Component } from '@angular/core' ; @Component({ moduleId: module.id, selector: 'a-comp' , template: ` <b-comp [textContent]="AText"></b-comp> <span [textContent]="AText"></span> ` }) export class AComponent { AText = 'some' ; }

您无需对跨度执行任何其他操作,但是对于b-comp,您需要指示其接收到textContent属性:

@Component({ selector: 'b-comp', template: 'Comes from parent: {{ textContent}} ' }) export class BComponent { @Input() textContent; }

现在,只要A组件上的AText属性发生更改,Angular就会自动更新B组件上的textContent属性和跨度。 它还将为子组件调用ngOnChanges生命周期挂钩。

您可能想知道Angular如何知道BComponent和span支持textContent绑定。 对于简单的DOM元素,它在解析模板时由编译器使用的dom_element_schema_registry中定义。 对于组件和指令,它检查附加到该类的元数据,并确保bounded属性在输入装饰器属性中列出。 如果未找到绑定属性,则编译器将引发错误:

无法绑定到“文本”,因为它不是…的已知属性

这是一个很好的文档功能,对其理解应该没有问题。 现在让我们看看内部发生了什么。

工厂特定信息

重要的是要理解,尽管我们在B上指定了输入绑定并跨越了输入更新的所有相关信息,但它们都是在父A工厂中定义的。 让我们看一下为A组件生成的工厂:

function View_AComponent_0(_l) { return jit_viewDef1(0, [ jit_elementDef2(..., 'b-comp', ...), jit_directiveDef5(..., jit_BComponent6, [], { textContent: [0, 'textContent'] }, ...), jit_elementDef2(..., 'span', [], [[8, 'textContent', 0]], ...) ], function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.AText; var currVal_1 = 'd'; _ck(_v, 1, 0, currVal_0, currVal_1); }, function (_ck, _v) { var _co = _v.component; var currVal_2 = _co.AText; _ck(_v, 2, 0, currVal_2); }); }

如果您已阅读开始时提到的文章,那么工厂中的所有视图节点都应该已经熟悉。 前两个jit_elementDef2和jit_directiveDef5是构成B组件的元素和指令定义节点。 第三是跨度的元素定义。

节点定义绑定

您可能已经看到的使该工厂与其他工厂区分开的一件事是这些节点定义采用的参数。 我们的jit_directiveDef5在这里收到一个新参数:

jit_directiveDef5(..., jit_BComponent6, [], { textContent: [0, 'textContent'] } , ...),

从directiveDef函数参数列表中可以看到,此参数称为props

directiveDef(..., props?: {[name: string]: [number, string]} , ...)

它是带有键的对象,其中每个键定义一个绑定索引和要更新的属性名称。 在我们的示例中,textContent属性只有一个绑定:

{textContent: [0, 'textContent']}

例如,如果我们的指令接收到多个绑定,例如:

<b-comp [textContent]="AText" [otherProp]="AProp">

props参数将包含两个属性:

jit_directiveDef5(49152, null, 0, jit_BComponent6, [], { textContent: [0, 'textContent'], otherProp: [1, 'otherProp'] }, null),

当Angular创建指令定义节点时,它将使用这些值为视图节点生成绑定 。 在更改检测期间,每个绑定确定Angular应该用来更新节点并提供上下文信息的操作类型。 绑定类型是使用bindings标志设置的。 对于每个绑定的属性更新,编译器将设置以下标志:

export const enum BindingFlags { TypeProperty = 1 << 3,

并且由于我们在span元素上也有绑定,因此编译器也会生成提供给span元素定义的props参数:

jit_elementDef2(..., 'span', [], [[8, 'textContent', 0]] , ...)

对于元素,此参数的结构略有不同-一组道具。 该范围只有一个输入绑定,因此只有一个子数组。 数组中的第一个数字指定绑定要使用的操作类型-这是属性更新:

export const enum BindingFlags { TypeProperty = 1 << 3, // 8

以下是其他可能的变体,它们在Angular的DOM更新机制中进行了说明:

TypeElementAttribute = 1 << 0, TypeElementClass = 1 << 1, TypeElementStyle = 1 << 2,

编译器没有在指令的props中提供操作类型,因为只能为指令更新属性,因此所有绑定都设置为BindingFlags.TypeProperty。

更新渲染器和更新指令

编译器还在工厂中生成了两个函数:

function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.AText; var currVal_1 = _co.AProp; _ck(_v, 1, 0, currVal_0, currVal_1); }, function (_ck, _v) { var _co = _v.component; var currVal_2 = _co.AText; _ck(_v, 2, 0, currVal_2); }

您应该已经从有关DOM更新的文章中熟悉了第二个updateRenderer函数。 第一个函数称为updateDirectives。 这两个功能均由ViewUpdateFn接口定义。 有趣的是,它们看起来几乎相同。 它们都带有两个参数_ck和v,它们分别引用相同的实体。 那么为什么要有两个功能呢?

好吧,因为在更改检测期间有两个不同的操作:

并且这些操作在更改检测周期的不同阶段执行。 因此Angular具有两个函数,每个函数专门针对特定的节点定义,并在检查组件时在不同阶段调用它们:

  • updateDirectives —对指令(directiveDef)执行更新,并在检查开始时调用
  • updateRenderer —对DOM元素(elementDef)执行更新,并在检查过程中调用

每当Angular对某个组件执行更改检测,并且更改检测机制为该功能提供参数时,都会执行这两个功能。 现在,让我们看看这些功能的作用。

_ck是检查的缩写,它引用函数prodCheckAndUpdate 。 另一个参数是带有节点的组件视图。 这些函数的主要任务是从组件实例中检索绑定属性的当前值,并调用_ck函数,以传递视图,节点索引和检索到的值。 nodeIndex是应对其执行更改检测的视图节点的索引。 需要了解的重要一点是,Angular分别为每个视图节点执行DOM更新-这就是为什么需要节点索引的原因。 例如,如果我们有两个范围和两个指令:

<b-comp [textContent]="AText"></b-comp> <b-comp [textContent]="AText"></b-comp> <span [textContent]="AText"></span> <span [textContent]="AText"></span>

编译器将为updateRenderer和updateDirectives函数生​​成以下主体:

function(_ck, _v) { var _co = _v.component; var currVal_0 = _co.AText; // update first component _ck(_v, 1, 0, currVal_0); var currVal_1 = _co.AText; // update second component _ck(_v, 3, 0, currVal_1); }, function(_ck, _v) { var _co = _v.component; var currVal_0 = _co.AText; // update first component _ck(_v, 1, 0, currVal_0); var currVal_1 = _co.AText; // update second component _ck(_v, 3, 0, currVal_1); }, function(_ck, _v) { var _co = _v.component; var currVal_2 = _co.AText; // update first span _ck(_v, 4, 0, currVal_2); var currVal_3 = _co.AText; // update second span _ck(_v, 5, 0, currVal_3); } function(_ck, _v) { var _co = _v.component; var currVal_2 = _co.AText; // update first span _ck(_v, 4, 0, currVal_2); var currVal_3 = _co.AText; // update second span _ck(_v, 5, 0, currVal_3); }

确实没有太多事情发生。 功能的关键不在这两个功能之外。 让我们看看这个功能是什么。

更新DOM元素的属性

上面我们了解到,在更改检测期间,将由编译器生成的updateRenderer函数用作组件工厂的一部分,以更新DOM元素上的输入属性。 我提到它在更改检测期间通过_ck函数传递,并且此参数引用checkAndUpdate 。 这是一个简短的泛型函数,可以进行大量调用,最终执行checkAndUpdateElement 。 该函数基本上检查绑定是否为有角度的特殊形式[attr.name,class.name,style.some]或某些特定于节点的属性:

case BindingFlags. TypeElementAttribute -> setElementAttribute case BindingFlags. TypeElementClass -> setElementClass case BindingFlags. TypeElementStyle -> setElementStyle case BindingFlags. TypeProperty -> setElementProperty;

这是我们上面探讨的绑定所使用的地方。 由于绑定设置为BindingFlags.TypeProperty,因此将使用setElementProperty函数。 它内部仅调用渲染器的setProperty方法来更新DOM元素上的属性。

更新指令的属性

作为上一节中介绍的updateRenderer函数的补充,编译器将updateDirective函数添加到组件工厂,该工厂用于更新组件上的输入属性。 与updateRenderer一样,它在更改检测期间通过_ck函数传递,并且此参数引用checkAndUpdate 。 仅这次使用checkAndUpdateDirective函数执行更新。 这是因为我们正在更新标记为NodeFlags.TypeDirective的节点上的属性。 该函数执行以下操作:

  1. 从视图节点检索组件/指令类实例
  2. 检查属性是否更改
  3. 如果值更改:
    一个。 更新类实例上的相关属性
    b。 准备SimpleChange数据并更新oldValues
    C。 如果组件使用OnPush策略,则将状态设置为checksEnabled
    d。 调用ngOnChanges生命周期挂钩
  4. 调用ngOnInit生命周期挂钩(如果是第一次检查视图)
  5. 调用ngDoCheck生命周期挂钩

当然,只有在组件/指令类上定义了所有生命周期挂钩之后,才会调用它们。 Angular为此使用了上面显示的nodeDef标志。 您可以从以下代码中看到它:

if (... && (def. flags & NodeFlags. OnInit )) { directive.ngOnInit(); } if (def. flags & NodeFlags. DoCheck ) { directive.ngDoCheck(); }

与DOM更新一样,所有以前的值都存储在视图上的view的oldValues属性中。 而已!

您发现文章中的信息有帮助吗?

From: https://hackernoon.com/the-mechanics-of-property-bindings-update-in-angular-39c0812bc4ce

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值