2

阅读本文需要已经对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输出以下代码:

<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初始化仅仅是为了说明这一点,接下来可以删掉这个绑定了。

务必要区分“初始化 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的实例对象中的idproperty需要被绑定更新
但是什么时候更新呢?用什么数据来更新呢?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); }
  1. 用vscode追踪一下,很快就能发现这个函数被存储在了ViewDefinition.updateDirectives中。
  2. 然后,Service.updateDirectives会调用ViewDefinition.updateDirectives函数,并根据checkType提供不同的参数,不妨假设提供的参数是(prodCheckAndUpdateNode, view),也就是说,function (_ck, _v)的实参是它。
  3. 好,调用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语句。
  4. prodCheckAndUpdateNode的作用仅仅是利用viewcheckIndex参数来获取具有绑定的那个node(checkIndex为1也就表示i1.ɵdid(1, 49152, null, 0, i3.BComponent, [], { id: [0, "id"] }, null)这个directive node),然后把锅全部丢给了checkAndUpdateNode
  5. checkAndUpdateNode的作用仅仅是根据argStyle决定传递参数的方式,要一个一个地传递参数还是传入一个数组(前者速度更快,但最多只能传10个value)。假设传递一个数组,也就是说checkAndUpdateNode决定要调用checkAndUpdateNodeDynamic
  6. checkAndUpdateNodeDynamic中,判断需要更新的node的类型,然后根据node类型调用不同的处理函数。在这个例子中是directive node,也就是说要调用checkAndUpdateDirectiveDynamic
  7. 到了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;
}

  1. 先从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的直接孩子。
  2. 对于这个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的调用时机
  3. 如果条件合适,调用这个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


csRyan
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.


引用和评论

0 条评论