本篇文章主要的讲述了关于angularjs属性绑定更新机制,还有angularjs更新属性的详解,都在这篇文章中,现在就让我们一起来看这篇文章吧
angularjs属性绑定更新机制解释:
所有现代前端框架都是用组件来合成 ui,这样很自然就会产生父子组件层级,这就需要框架提供父子组件通信的机制。同样,angular 也提供了两种方式来实现父子组件通信:输入输出绑定和共享服务。对于 stateless presentational components 我更喜欢输入输出绑定方式,然而对于 stateful container components 我使用共享服务方式。
本文主要介绍输入输出绑定方式,特别是当父组件输入绑定值变化时,angular 如何更新子组件输入值。如果想了解 angular 如何更新当前组件 dom,可以查看 译 angular dom 更新机制,这篇文章也会有助于加深对本文的理解。由于我们将探索 angular 如何更新 dom 元素和组件的输入绑定属性,所以假定你知道 angular 内部是如何表现组件和指令的,如果你不是很了解并且很感兴趣,可以查看 译 为何 angular 内部没有发现组件, 这篇文章主要讲了 angular 内部如何使用指令形式来表示组件。而本文对于组件和指令两个概念互换使用,因为 angular 内部就是把组件当做指令。
模板绑定语法你可能知道 angular 提供了 属性绑定语法 —— [],这个语法很通用,它可以用在子组件上,也可以用在原生 dom 元素上。如果你想从父组件把数据传给子组件 b-comp 或者原生 dom 元素 span,你可以在父组件模板中这么写:
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';}
你不必为原生 dom 元素做些额外的工作,但是对于子组件 b-comp 你需要申明输入属性 textcontent:
@component({ selector: 'b-comp', template: 'comes from parent: {{textcontent}}'})export class bcomponent { @input() textcontent;}
这样当父组件 acomponent.atext 属性改变时,angular 会自动更新子组件 bcomponent.textcontent 属性,和原生元素 span.textcontent 属性。同时,还会调用子组件 bcomponent 的生命周期钩子函数 ngonchanges(注:实际上还有 ngdocheck,见下文)。
你可能好奇 angular 是怎么知道 bcomponent 和 span 支持 textcontent 绑定的。这是因为 angular 编译器在解析模板时,如果遇到简单 dom 元素如 span,就去查找这个元素是否定义在 dom_element_schema_registry,从而知道它是 htmlelement 子类,textcontent 是其中的一个属性(注:可以试试如果 span 绑定一个 [abc]=atext 就报错,没法识别 abc 属性);如果遇到了组件或指令,就去查看其装饰器 @component/@directive 的元数据 input 属性里是否有该绑定属性项,如果没有,编译器同样会抛出错误:
can’t bind to ‘textcontent’ since it isn’t a known property of …
这些知识都很好理解,现在让我们进一步看看其内部发生了什么。
组件工厂尽管在子组件 bcomponent 和 span 元素绑定了输入属性,但是输入绑定更新所需要的信息全部在父组件 acomponent 的组件工厂里。让我们看下 acomponent 的组件工厂代码:
function view_acomponent_0(_l) { return jit_viewdef1(0, [ jit_elementdef_2(..., 'b-comp', ...), jit_directivedef_5(..., jit_bcomponent6, [], { textcontent: [0, 'textcontent'] }, ...), jit_elementdef_2(..., '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); });}
如果你读了 译 angular dom 更新机制 或 译 为何 angular 内部没有发现组件,就会对上面代码中的各个视图节点比较熟悉了。前两个节点中,jit_elementdef_2 是元素节点,jit_directivedef_5 是指令节点,这两个组成了子组件 bcomponent;第三个节点 jit_elementdef_2 也是元素节点,组成了 span 元素。(想看更多就到angularjs开发手册中学习)
节点绑定相同类型的节点使用相同的节点定义函数,但区别是接收的参数不同,比如 jit_directivedef_5 节点定义函数参数如下:
jit_directivedef_5(..., jit_bcomponent6, [], { textcontent: [0, 'textcontent']}, ...),
其中,参数 {textcontent: [0, 'textcontent']} 叫做 props,这点可以查看 directivedef 函数的参数列表:
directivedef(..., props?: {[name: string]: [number, string]}, ...)
props 参数是一个对象,每一个键为绑定属性名,对应的值为绑定索引和绑定属性名组成的数组,比如本例中只有一个绑定,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 会使用这些值来生成当前指令节点的 binding,从而生成当前视图的指令节点。在变更检测时,每一个 binding 决定 angular 使用哪种操作来更新节点和提供上下文信息,绑定类型是通过 bindingflags 设置的(注:每一个绑定定义是 bindingdef,它的属性 flags: bindingflags 决定 angular 该采取什么操作,比如 class 型绑定和 style 型绑定都会调用对应的操作函数,见下文)。比如,如果是属性绑定,编译器会设置绑定标志位为:
export const enum bindingflags { typeproperty = 1 << 3,
注:上文说完了指令定义函数的参数,下面说说元素定义函数的参数。本例中,因为 span 元素有属性绑定,编译器会设置绑定参数为 [[8, 'textcontent', 0]]:
jit_elementdef2(..., 'span', [], [[8, 'textcontent', 0]], ...)
不同于指令节点,对元素节点来说,绑定参数结构是个二维数组,因为 span 元素只有一个绑定,所以它仅仅只有一个子数组。数组 [8, 'textcontent', 0] 中第一个参数也同样是绑定标志位 bindingflags,决定 angular 应该采取什么类型操作(注:[8, 'textcontent', 0] 中的 8 表示为 property 型绑定):
export const enum bindingflags { typeproperty = 1 << 3, // 8
其他类型标志位已经在文章 译 angular dom 更新机制 有所解释:
typeelementattribute = 1 << 0,typeelementclass = 1 < setelementstylecase bindingflags.typeproperty -> setelementproperty;
上面代码就是刚刚说的几个绑定类型,当绑定标志位是 bindingflags.typeproperty,会调用函数 setelementproperty,该函数内部也是通过调用 dom renderer 的 setproperty 方法来更新 dom。
注:setelementproperty 函数里这行代码 view.renderer.setproperty(rendernode,name, rendervalue);,renderer 就是 renderer2 interface,它仅仅是一个接口,在浏览器平台下,它的实现就是 defaultdomrenderer2。更新指令的属性上文中已经描述了 updaterenderer 函数是用来更新元素的属性,而 updatedirective 是用来更新子组件的输入绑定属性,并且变更检测期间传入的参数 _ck 就是函数 prodcheckandupdatenode。只是进过一系列函数调用后,最终调用的函数却是checkandupdatedirectiveinline,这是因为这次节点的标志位是 nodeflags.typedirective
checkandupdatedirectiveinline 函数主要功能如下:
从当前视图节点里获取组件/指令对象
检查组件/指令对象的绑定属性值是否发生改变
如果属性发生改变:
a. 如果变更策略设置为 onpush,设置视图状态为 checksenabled
b. 更新子组件的绑定属性值
c. 准备 simplechange 数据和更新视图的 oldvalues 属性,新值替换旧值
d. 调用生命周期钩子 ngonchanges
如果该视图是首次执行变更检测,则调用生命周期钩子 ngoninit
调用生命周期钩子 ngdocheck
当然,只有在生命周期钩子在组件内定义了才被调用,angular 使用 nodedef 节点标志位来判断是否有生命周期钩子,如果查看源码你会发现类似如下代码:
if (... && (def.flags & nodeflags.oninit)) { directive.ngoninit();}if (def.flags & nodeflags.docheck) { directive.ngdocheck();}
和更新元素节点一样,更新指令时也同样把上一次的值存储在视图数据的属性 oldvalues 里(注:即上面的 3.c 步骤)。
好了,本篇文章到这就结束了(想看更多就到angularjs使用手册中学习),有问题的可以在下方留言提问。
以上就是[译] angular 属性绑定更新机制 - laravel/angular 技术分享的详细内容。