Cricket 2019-06-26
阅读本文需要已经对ngc输出的代码、Angular packages/core源码有所熟悉。
这是我搭建的一个直接可以使用的Angular aot demo项目,具体功能和用法可以看README,我认为它对于深入学习Angular源码十分有帮助。本文也使用这个项目开始做实验。
另外需要注意的一点是,Component是一种特殊的(带有view的)Directive,本文的讨论完全适用于Component。
在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输出以下代码:
<b-comp [account-id]="bindingVal" account-id='attribute binding value'></b-comp>
==>
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初始化仅仅是为了说明这一点,接下来可以删掉这个绑定了。
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); }
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。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
中。
如果条件合适,调用这个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