Angular源码分析:使用数组来存储节点树、对它进行后序遍历

雾桩接口
• 阅读 2635

使用数组来存储节点树

虽然DOM是以树的方式来组织,但Angular是以平数组的方式来存储view中各个节点(包括DOM节点,也包括Directive节点、Provider节点等)的。
数组中每个节点有一个childCount属性,表示在这个数组项的后面有多少个节点是它的后代。
举个例子,下面的平数组

[
{tagName:'ul',childCount:4},
{tagName:'li',childCount:2},
{tagName:'span',childCount:0},{tagName:'span',childCount:0},
{tagName:'li',childCount:0},

{tagName:'div',childCount:1},
{tagName:'span',childCount:0}
]

就能表示这样一个树形结构:

<ul>
    <li>
        <span></span>
        <span></span>
    </li>
    <li></li>
</ul>
<div>
    <span></span>
</div>
平数组的“平”是flat的意思,表示这个数据结构是没有嵌套的。(用嵌套数组也可以表示上面的DOM树结构,比如说使用一个children属性来存储子节点组成的数组,但是这会占用更多的内存)

至于原因,TOBIAS BOSCH在某次会议中给出了回答:如果以树的方式存储,遍历它的时候,不管使用递归还是非递归的方法,都要维护一个栈。而如果用flat数组方式存储这颗树,使用一个for循环就能遍历,遍历速度更快,且遍历需要更少的内存(仅仅需要维护一个迭代的数组下标i)。
除了遍历时的性能以外,平数组本身也占用更少的内存(因为数组没有嵌套)。

后序遍历

树用平数组表示以后也能进行后序遍历,Angular在callLifecycleHooksChildrenFirst方法中就实现了后序遍历算法(非递归)。
这个方法的作用是:按照后序的顺序,调用view中各个directive、component的生命周期钩子(仅包括ngAfterContentCheck, ngAfterContentInit, ngAfterViewCheck, ngAfterViewInit,其他生命周期钩子不在这里调用)。
在这篇文章我们先不研究这些钩子,我们仅仅专注于后序遍历是怎么实现的。

export function callLifecycleHooksChildrenFirst(view: ViewData, lifecycles: NodeFlags) {
  if (!(view.def.nodeFlags & lifecycles)) {
    return;
  }
  const nodes = view.def.nodes;
  let initIndex = 0;
  for (let i = 0; i < nodes.length; i++) {
    const nodeDef = nodes[i];
    let parent = nodeDef.parent;
    if (!parent && nodeDef.flags & lifecycles) {
      // matching root node (e.g. a pipe)
      callProviderLifecycles(view, i, nodeDef.flags & lifecycles, initIndex++);
    }
    if ((nodeDef.childFlags & lifecycles) === 0) {
      // no child matches one of the lifecycles
      i += nodeDef.childCount;
    }
    while (parent && (parent.flags & NodeFlags.TypeElement) &&
           i === parent.nodeIndex + parent.childCount) {
      // last child of an element
      if (parent.directChildFlags & lifecycles) {
        initIndex = callElementProvidersLifecycles(view, parent, lifecycles, initIndex);
      }
      parent = parent.parent;
    }
  }
}

第一个判断语句的意思是,如果此view中的所有directive都没有定义我们要调用的lifecycles,直接返回。(view.def.nodeFlags聚合了view中所有节点的nodeFlags)
initIndex的作用是记录view已经对哪些节点调用过了init lifecycle hooks,以防止从错误处理中恢复以后第二次调用节点的init钩子。在这篇文章可以不用管它。

接下来就是在for循环中遍历view中的每个节点了,对每个节点nodeDef都获取到它的parent

可以推理出nodeDef的下标在[parent.nodeIndex+1, parent.nodeIndex + parent.childCount]范围内。

循环中的第一个if语句:

    if (!parent && nodeDef.flags & lifecycles) {
      // matching root node (e.g. a pipe)
      callProviderLifecycles(view, i, nodeDef.flags & lifecycles, initIndex++);
    }

假如遇到了没有parent的节点,说明它是view的根节点之一,我们要访问它(也就是调用它的lifecycles)。

注意,一般来说后序遍历要求我们在访问节点之前应该确认它的所有子节点已经被访问过(或者根本就没有子节点)。但由于在Angular中,view中的根节点如果有lifecycles可以调用的话,这个节点只能是pipe或者provider,这种节点都不可能有子节点,因此后序遍历可以直接访问这个根节点。

循环中的第二个if语句:

    if ((nodeDef.childFlags & lifecycles) === 0) {
      // no child matches one of the lifecycles
      i += nodeDef.childCount;
    }

如果(nodeDef.childFlags & lifecycles) === 0,也就是说这个nodeDef的所有后代节点都没有符合的lifecycles方法可以调用(用户没有定义ngAfterViewCheck这些方法)(nodeDef.childFlags聚合了nodeDef所有后代节点的nodeFlags),则可以直接跳过nodeDef的所有后代节点i += nodeDef.childCount,这个语句直接增加for循环的迭代下标,使for循环跳过nodeDef的所有后代节点。

平数组的迭代就用这样的方式来做“剪枝”(直接跳过某个节点下面的子树)。Angular迭代平数组的时候经常使用这种技巧。

循环中的嵌套循环:

    while (parent && (parent.flags & NodeFlags.TypeElement) &&
           i === parent.nodeIndex + parent.childCount) {
      // last child of an element
      if (parent.directChildFlags & lifecycles) {
        initIndex = callElementProvidersLifecycles(view, parent, lifecycles, initIndex);
      }
      parent = parent.parent;
    }

i === parent.nodeIndex + parent.childCount成立的时候,意味着当前迭代的节点nodeDefparent最后一个后代节点。根据后序遍历的要求,因为parent的所有后代节点都已经访问过,所以此时应该访问parent节点(调用它的钩子)。
访问完parent以后,parent = parent.parent;找到更上层的父节点,继续做i === parent.nodeIndex + parent.childCount的检查(nodeDef可能同时是多个祖先节点的最后一个后代,比如{tagName:'ul',childCount:2},{tagName:'li',childCount:1},{tagName:'span',childCount:0}中,<span>同时是<ul>和<li>的最后一个后代)。

这样继续迭代下去,就能完成对view中所有节点的后序遍历。可以看出,这个后序遍历算法的时间复杂度是O(n)(内层循环不增加复杂度)。

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这