阅读本文需要已经对ngc输出的代码、Angular packages/core源码有所熟悉。
这是我搭建的一个直接可以使用的Angular aot demo项目,具体功能和用法可以看README,我认为它对于深入学习Angular源码十分有帮助。本文也使用这个项目开始做实验。
另外需要注意的一点是,Component是一种特殊的(带有view的)Directive,本文的讨论完全适用于Component。
directive inputs
在Metadata中指定inputs等价于在Class中使用@Input装饰器,Angular Compiler输出的代码完全相同。
Directive inputs的本质是:将Directive实例对象中的某个property与父视图(parent view)中的某个表达式进行数据绑定,在每个变化检测周期,比较这里两个值是否相等,如果不相等,则更新Directive实例对象中的这个property。
其他类型的数据绑定也是类似的,比如绑定template中某个普通HTML元素的id、class。
我创建了一个最基本的demo仓库来展示directive的input是如何实现的,读者可以克隆下来自己根据README指引用ngc编译:angular-directive-interactive-demo
输入命令行指令npm run dev
,ngc为AppComponent的view输出以下代码:
==>
export function View_AppComponent_0(_l) { return i1.ɵvid(0, [(_l()(), i1.ɵeld(0, 0, null, null, 1, "b-comp", [["account-id", "attribute binding value"]], null, null, null, i2.View_BComponent_0, i2.RenderType_BComponent)), i1.ɵdid(1, 49152, null, 0, i3.BComponent, [], { id: [0, "id"] }, null)], function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.bindingVal; _ck(_v, 1, 0, currVal_0); }, null); }
[["account-id", "attribute binding value"]]
表示在这个元素上的设置了attribute。注意,当property binding与attribute同时匹配一个directive的输入时,property binding优先作为输入。我在template中进行account-id='attribute binding value'
attribute初始化仅仅是为了说明这一点,接下来可以删掉这个绑定了。
务必要区分“ 初始化 HTML attribute”(比如account-id="attribute binding value"
)与“绑定 DOM property”(绑定 DOM property 有两种方式:[account-id]="bindingVal"
或account-id="{{bindingVal}}"
,注意1. 这两种property binding的编译输出有区别;2. 第二种property binding的形式与“初始化 HTML attribute”很相似,区别在于有没有双花括号)。官方文档: HTML attribute vs. DOM property。另外,Angular 其实也能绑定HTML attribute。
[attr.account-id]='"attribute binding value"'
和上面初始化attribute的效果相同,但是绑定更加强大,你可以将它与component中的一个property绑定,使attribute随着property更新。如果你的CSS中有[attribute=value]
这样的CSS选择器,HTML attribute binding或许可以帮到你(这种情况比较少)。大多数情况下,我们仅仅需要初始化element或directive的attribute。
{ id: [0, "id"] }
在directiveDef中被转化成了property binding的记号(flags: BindingFlags.TypeProperty
),它表示了当前directive node的实例对象中的id
property需要被绑定更新。
但是什么时候更新呢?用什么数据来更新呢?NodeDef并没有定义这些,也不应该定义这些,根据Single responsibility principle,单个NodeDef只负责定义这个Node的属性和行为,而“什么时候更新、用什么数据来更新”已经超越了这个node的范畴,它们由viewDef的updateDirectives参数来指定。
确实,从ngc输出的代码中,我们看到这个参数是
function (_ck, _v) { var _co = _v.component; var currVal_0 = _co.bindingVal; _ck(_v, 1, 0, currVal_0); }
- 用vscode追踪一下,很快就能发现这个函数被存储在了ViewDefinition.updateDirectives中。
- 然后,Service.updateDirectives会调用
ViewDefinition.updateDirectives
函数,并根据checkType
提供不同的参数,不妨假设提供的参数是(prodCheckAndUpdateNode, view)
,也就是说,function (_ck, _v)
的实参是它。 - 好,调用
ViewDefinition.updateDirectives
的实参已经确定了,那么调用它会发生什么呢?前两个语句var _co = _v.component; var currVal_0 = _co.bindingVal;
很简单:_co是当前view的component实例(也就是AppComponent的实例,即Model-View-Whatever架构模式中的Model),currVal_0是Model中的一个数据。这就回答了“用什么数据来更新呢”的问题,用AppComponent(parent component)实例的bindingVal来更新BComponent(child directive)的@input property。
检查和更新绑定的逻辑都在第三个语句_ck(_v, 1, 0, currVal_0);
中。我们前面已经说过了,_ck的实参是prodCheckAndUpdateNode。注意到_ck
的返回值没有被使用,所以可以忽略prodCheckAndUpdateNode
的return语句。 -
prodCheckAndUpdateNode
的作用仅仅是利用view
和checkIndex
参数来获取具有绑定的那个node(checkIndex为1也就表示i1.ɵdid(1, 49152, null, 0, i3.BComponent, [], { id: [0, "id"] }, null)
这个directive node),然后把锅全部丢给了checkAndUpdateNode。 -
checkAndUpdateNode
的作用仅仅是根据argStyle
决定传递参数的方式,要一个一个地传递参数还是传入一个数组(前者速度更快,但最多只能传10个value)。假设传递一个数组,也就是说checkAndUpdateNode
决定要调用checkAndUpdateNodeDynamic。 - 在
checkAndUpdateNodeDynamic
中,判断需要更新的node的类型,然后根据node类型调用不同的处理函数。在这个例子中是directive node,也就是说要调用checkAndUpdateDirectiveDynamic。 - 到了checkAndUpdateDirectiveDynamic,我们终于看到directive property更新的逻辑了:
export function checkAndUpdateDirectiveDynamic(
view: ViewData, def: NodeDef, values: any[]): boolean {
const providerData = asProviderData(view, def.nodeIndex);
const directive = providerData.instance;
let changed = false;
let changes: SimpleChanges = undefined !;
for (let i = 0; i < values.length; i++) {
if (checkBinding(view, def, i, values[i])) {
changed = true;
changes = updateProp(view, providerData, def, i, values[i], changes);
}
}
if (changes) {
directive.ngOnChanges(changes);
}
if ((def.flags & NodeFlags.OnInit) &&
shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
return changed;
}
-
先从viewdata获取到这个directive的实例(BComponent实例):
const providerData = asProviderData(view, def.nodeIndex); const directive = providerData.instance;
为什么directive和provider扯上了关系?你应该知道在child directive中可以通过依赖注入获取parent directive实例,这都是因为 Angular将directive看作一种服务,这种服务由宿主元素提供!这也是为什么directive node必须是某个element node的直接孩子。
-
对于这个directive的每个input binding,检查绑定是否已经不一致(脏)。如果有,则更新directive中相应的property并记录这次更新在
changes
中。updateProp这个函数有一个地方比较有意思: 如果child node是使用OnPush变化检测策略的component,那么updateProp的调用(也就是说,有input binding被更新)会使这个component的view 被标记为“将要检查view”。可以料想到,如果这个OnPush component没有input binding更新,它的view不会被检查。
如果将变化检测看作是对 由若干个view组成的树的深度优先遍历,那么Angular可以通过“剪枝”(不检查OnPush component view以及它的child view)来优化变化检测的速度。
一个很常见的误解是:Angular在检查到一个directive时才去检查它的input binding,但这是错的。对 所有child directive的input binding进行脏检查是检查 parent view时的工作之一。检查完 parent view以后再检查 child view。这篇文章有所说明: view的检查过程以及ngDoCheck的调用时机。 -
如果条件合适,调用这个direvtice的Lifecycle Hooks:ngOnChanges, ngOnInit, ngDoCheck。
ngDoCheck Lifecycle Hooks的作用主要是针对OnPush component的。在ngDoCheck中扩展基本的脏检查算法。如前文所说,Angular只检查directive的input bingdings是否更新,如果有更新才将OnPush component标记为“将要检查view”。但如果input是一个对象,且发生变化的是对象中的一个property,那么默认Angular脏检查算法无法检测到这种变化,因为input始终是同一个对象引用。这时候你需要在ngDoCheck中自己检查input的某些property,如果发现脏绑定,用 ChangeDetectorRef.markForCheck手动将本component标记为“将要检查view”。
好,现在我们已经知道Service.updateDirectives
会调用ViewDefinition.updateDirectives
函数来检查和更新child directive的input binding。那么这种更新发生在什么时候?也就是说,Service.updateDirectives
自己是什么时候被调用的?被谁调用的?
答案是checkAndUpdateView,这个函数是变化检测的一个关键函数,有很多需要整理,我将在另一篇文章中讨论。
更多阅读
view的检查过程以及ngDoCheck的调用时机
The mechanics of DOM updates in Angular
The mechanics of property bindings update in Angular