Angular 进阶:从源码理解@Input绑定是如何被编译和实现的

前端之家收集整理的这篇文章主要介绍了Angular 进阶:从源码理解@Input绑定是如何被编译和实现的前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

阅读本文需要已经对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,null,1,"b-comp",[["account-id","attribute binding value"]],i2.View_BComponent_0,i2.RenderType_BComponent)),i1.ɵdid(1,49152,i3.BComponent,[],{ id: [0,"id"] },null)],function (_ck,_v) { var _co = _v.component; var currVal_0 = _co.bindingVal; _ck(_v,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,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,currVal_0);。我们前面已经说过了,_ck的实参是prodCheckAndUpdateNode。注意到_ck的返回值没有被使用,所以可以忽略prodCheckAndUpdateNode的return语句。
  4. prodCheckAndUpdateNode的作用仅仅是利用viewcheckIndex参数来获取具有绑定的那个node(checkIndex为1也就表示i1.ɵdid(1,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,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

猜你在找的Angularjs相关文章