vapor 中的 ast 是如何被 transform 到 IR 的

上一篇文章【vue3 的 parse 都做了啥】中已经介绍了 vapor 编译的三个阶段的第一个阶段 parse

compile 有三个阶段,分别为 parse,transform,generate

那么本篇文章就介绍第二个阶段 transform

从代码中可以看到,transform 负责将 parse 得到的 ast 转换为 IR,IR 是中间表示,同样的 template 代码如下

vue 复制代码
<template>
    <h2 v-if="true">{{ title }}</h2>
</template>

若是 v3(vdom),那么被编译后的 render 函数会是这样的

js 复制代码
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("template", null, [
    true
      ? (_openBlock(), _createElementBlock("h2", { key: 0 }, _toDisplayString(_ctx.title), 1 /* TEXT */))
      : _createCommentVNode("v-if", true)
  ]))
}

而 vapor 模式下,则是下面这样

js 复制代码
export function render(_ctx, $props, $emit, $attrs, $slots) {
  const n3 = t1()
  _setInsertionState(n3)
  const n0 = _createIf(() => (true), () => {
    const n2 = t0()
    const x2 = _child(n2)
    _renderEffect(() => _setText(x2, _toDisplayString(_ctx.title)))
    return n2
  })
  return n3
}

可以看到 vapor 的 render 函数里面是这种 t,n 的块状结构

在 v3 中,想要编译成 render 函数,会在 parse 得到 template AST 后,还会经历一次 transform 得到 transform AST,后者会将 v- 指令进行编译,最后再是 generate 成 render 函数

而 vapor 中,transform 得到的不再是 transform AST,而是 IR,可以看到最后的 render 函数是 n2 = t0() 这种 block 块,想要生成这种块,则需要 IR 的支持

transform

ts 复制代码
  const ir = transform(
    ast,
    extend({}, resolvedOptions, {
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []), // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {}, // user transforms
      ),
    }),
  )

仔细看这段代码,会发现这和 v3 的 transform AST 生成阶段非常相似

第一个入参为 template AST,第二个入参则是 v- 指令,v- 指令在这之前被分成了两个部分,nodeTransforms 和 directiveTransforms

他们来自 getBaseTransformPreset

一个是结构形指令,一个是普通指令,前者像是 v-if 这种会改变 dom 结构,后者不改变 dom 结构,只是添加属性,事件或者样式

现在我们进入 transform 函数中

可以非常清晰得看到 IR 的结构,里面有 ir 类型,ast,source 源码,指令信息,block 信息等等;transform 先是通过 TransformContext 构造器实例化了 上下文对象 context,然后将 context 给到 transformNode 去得到 ir 对象。

我们先看下 transformNode 的 结构

ts 复制代码
  constructor(
    public ir: RootIRNode,
    public node: T,
    options: TransformOptions = {},
  )

ir 的 接口为 RootIRNode,node 则是 template AST,options 则是 v- 指令

我们现在看下 RootIRNode 接口

ts 复制代码
export interface RootIRNode {
  type: IRNodeTypes.ROOT
  node: RootNode
  source: string
  template: string[]
  rootTemplateIndex?: number
  component: Set<string>
  directive: Set<string>
  block: BlockIRNode
  hasTemplateRef: boolean
}

里面有个 type,再看下 IRNodeTypes 的接口

ts 复制代码
export enum IRNodeTypes {
  ROOT,
  BLOCK,

  SET_PROP, // 设置静态属性
  SET_DYNAMIC_PROPS, 
  SET_TEXT,
  SET_EVENT,
  SET_DYNAMIC_EVENTS, // 批量设置动态事件
  SET_HTML,
  SET_TEMPLATE_REF,

  INSERT_NODE,
  PREPEND_NODE,
  CREATE_COMPONENT_NODE,
  SLOT_OUTLET_NODE,

  DIRECTIVE,
  DECLARE_OLD_REF, // consider make it more general

  IF,
  FOR,

  GET_TEXT_CHILD,
}

每个 IR 类型都代表一个具体的运行时操作,比如 SET_PROP 就是 setAttribute

回到 transform 函数,你会发现 transform 函数主要就是依靠 transformNode 函数,现在我们看下 transformNode 函数

transformNode

js 复制代码
export function transformNode(
  context: TransformContext<RootNode | TemplateChildNode>,
): void {
  let { node } = context

  // apply transform plugins
  const { nodeTransforms } = context.options
  const exitFns = []
  for (const nodeTransform of nodeTransforms) {
    const onExit = nodeTransform(node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.node) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.node
    }
  }

  // exit transforms
  context.node = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }

  if (context.node.type === NodeTypes.ROOT) {
    context.registerTemplate()
  }
}

transformNode 负责将单个 AST 节点转换为对应的 IR 操作

这段代码和 v3 的 transformed AST 也非常相似

let { node } = context 拿到了当前的 AST 节点

const { nodeTransforms } = context.options 拿到了结构性指令,也就是上面提到的能够改变 dom 结构的 v- 指令

const exitFns = [] 则是收集 退出函数,这些退出函数会在遍历 ast 节点时挨个 push,最后再倒着执行

ts 复制代码
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }

我们先看下面的 template 会得到什么样的 ast

vue 复制代码
<template>
    <h2 v-if="true">{{ title }}</h2>
</template>

经过 parse 后得到的 template AST 如下

js 复制代码
{
    "type": 0,
    "source": "<template>\r\n    <h2 v-if=\"true\">{{ title }}</h2>\r\n</template>\r\n",
    "children": [
        {
            "type": 1,
            "tag": "template",
            "ns": 0,
            "tagType": 0,
            "props": [],
            "children": [
                {
                    "type": 1,
                    "tag": "h2",
                    "ns": 0,
                    "tagType": 0,
                    "props": [
                        {
                            "type": 7,
                            "name": "if",
                            "rawName": "v-if",
                            "exp": {
                                "type": 4,
                                "loc": {
                                    "start": {
                                        "column": 15,
                                        "line": 2,
                                        "offset": 26
                                    },
                                    "end": {
                                        "column": 19,
                                        "line": 2,
                                        "offset": 30
                                    },
                                    "source": "true"
                                },
                                "content": "true",
                                "isStatic": false,
                                "constType": 0,
                                "ast": null
                            },
                            "modifiers": [],
                            "loc": {
                                "start": {
                                    "column": 9,
                                    "line": 2,
                                    "offset": 20
                                },
                                "end": {
                                    "column": 20,
                                    "line": 2,
                                    "offset": 31
                                },
                                "source": "v-if=\"true\""
                            }
                        }
                    ],
                    "children": [
                        {
                            "type": 5,
                            "content": {
                                "type": 4,
                                "loc": {
                                    "start": {
                                        "column": 24,
                                        "line": 2,
                                        "offset": 35
                                    },
                                    "end": {
                                        "column": 29,
                                        "line": 2,
                                        "offset": 40
                                    },
                                    "source": "title"
                                },
                                "content": "title",
                                "isStatic": false,
                                "constType": 0,
                                "ast": null
                            },
                            "loc": {
                                "start": {
                                    "column": 21,
                                    "line": 2,
                                    "offset": 32
                                },
                                "end": {
                                    "column": 32,
                                    "line": 2,
                                    "offset": 43
                                },
                                "source": "{{ title }}"
                            }
                        }
                    ],
                    "loc": {
                        "start": {
                            "column": 5,
                            "line": 2,
                            "offset": 16
                        },
                        "end": {
                            "column": 37,
                            "line": 2,
                            "offset": 48
                        },
                        "source": "<h2 v-if=\"true\">{{ title }}</h2>"
                    }
                }
            ],
            "loc": {
                "start": {
                    "column": 1,
                    "line": 1,
                    "offset": 0
                },
                "end": {
                    "column": 12,
                    "line": 3,
                    "offset": 61
                },
                "source": "<template>\r\n    <h2 v-if=\"true\">{{ title }}</h2>\r\n</template>"
            }
        }
    ],
    "helpers": {},
    "components": [],
    "directives": [],
    "hoists": [],
    "imports": [],
    "cached": [],
    "temps": 0,
    "loc": {
        "start": {
            "column": 1,
            "line": 1,
            "offset": 0
        },
        "end": {
            "column": 1,
            "line": 4,
            "offset": 63
        },
        "source": "<template>\r\n    <h2 v-if=\"true\">{{ title }}</h2>\r\n</template>\r\n"
    }
}

ast 里面有个 type 属性,由于是 enum 枚举类型,所以是数字,数字 0 就代表为 根节点,我们看下 NodeTypes 接口

ts 复制代码
export enum NodeTypes {
  ROOT,
  ELEMENT,
  TEXT,
  COMMENT,
  SIMPLE_EXPRESSION,
  INTERPOLATION,
  ATTRIBUTE,
  DIRECTIVE,
  // containers
  COMPOUND_EXPRESSION,
  IF,
  IF_BRANCH,
  FOR,
  TEXT_CALL,
  // codegen
  VNODE_CALL,
  JS_CALL_EXPRESSION,
  JS_OBJECT_EXPRESSION,
  JS_PROPERTY,
  JS_ARRAY_EXPRESSION,
  JS_FUNCTION_EXPRESSION,
  JS_CONDITIONAL_EXPRESSION,
  JS_CACHE_EXPRESSION,

  // ssr codegen
  JS_BLOCK_STATEMENT,
  JS_TEMPLATE_LITERAL,
  JS_IF_STATEMENT,
  JS_ASSIGNMENT_EXPRESSION,
  JS_SEQUENCE_EXPRESSION,
  JS_RETURN_STATEMENT,
}

接下来会针对当前的 ast 节点对每个 nodeTransform 函数执行,去收集 exitFns

ts 复制代码
  for (const nodeTransform of nodeTransforms) {
    const onExit = nodeTransform(node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.node) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.node
    }
  }

我们再来看下 nodeTransforms 数组

ts 复制代码
    [
      transformVOnce,
      transformVIf,
      transformVFor,
      transformSlotOutlet,
      transformTemplateRef,
      transformElement,
      transformText,
      transformVSlot,
      transformComment,
      transformChildren,
    ],

比如当前 ast 为 type 为 0 的 root 根节点,那么这个 root 会被所有的 nodeTransform 顺序执行,前面的函数都不会给根节点带来 onExit(先排除 transformElement) ,最后执行到 transformChildren 函数

transformChildren

js 复制代码
export const transformChildren: NodeTransform = (node, context) => {
  const isFragment =
    node.type === NodeTypes.ROOT ||
    (node.type === NodeTypes.ELEMENT &&
      (node.tagType === ElementTypes.TEMPLATE ||
        node.tagType === ElementTypes.COMPONENT))

  if (!isFragment && node.type !== NodeTypes.ELEMENT) return

  for (const [i, child] of node.children.entries()) {
    const childContext = context.create(child, i)
    transformNode(childContext)

    const childDynamic = childContext.dynamic

    if (isFragment) {
      childContext.reference()
      childContext.registerTemplate()

      if (
        !(childDynamic.flags & DynamicFlag.NON_TEMPLATE) ||
        childDynamic.flags & DynamicFlag.INSERT
      ) {
        context.block.returns.push(childContext.dynamic.id!)
      }
    } else {
      context.childrenTemplate.push(childContext.template)
    }

    if (
      childDynamic.hasDynamicChild ||
      childDynamic.id !== undefined ||
      childDynamic.flags & DynamicFlag.NON_TEMPLATE ||
      childDynamic.flags & DynamicFlag.INSERT
    ) {
      context.dynamic.hasDynamicChild = true
    }

    context.dynamic.children[i] = childDynamic
  }

  if (!isFragment) {
    processDynamicChildren(context as TransformContext<ElementNode>)
  }
}

transformChildren 函数目的就是更新当前节点为其子节点

只有 根节点,template,组件才会是 isFragment

然后遍历一个层级下的 所有 子节点,看到 返回 的 ast 可知,root 下面的子节点就是 template

因此这是个 深度优先遍历,处理第一个子节点时,会立即递归到他的所有后代,完全处理完第一个子节点的整个子树后,才会开始处理第二个子节点

const childContext = context.create(child, i) childContext 是子节点上下文,我们看下 create 是如何被定义的,create 是 上下文 TransformContext 身上的属性

可以看到,里面记录了当前节点,父节点信息,下标,以及 是否为动态属性等信息

Object.create(Transform.context.prototype) 创建的新对象可以访问 TransformContext 类身上的所有方法

newDynamic 默认创建的 dynamic 值 为 1

ts 复制代码
export enum DynamicFlag {
  NONE = 0,
  /**
   * This node is referenced and needs to be saved as a variable.
   */
  REFERENCED = 1,
  /**
   * This node is not generated from template, but is generated dynamically.
   */
  NON_TEMPLATE = 1 << 1,
  /**
   * This node needs to be inserted back into the template.
   */
  INSERT = 1 << 2,
}

可以看到 DynamicFlag 里面有四个标志位

none 代表静态节点,二进制 000

referenced 代表节点被引用,二进制 001,这个为 默认值

non_template 代表节点不是从模板生成的,二进制 010,一般是 v-if,v-for,动态组件

insert 代表节点需要被插回模版中,二进制 100,v-if 的内容需要插入到正确的 dom 中

回到 transformChildren 下一行又是调用 transformNode,此时的 ast 已经是 子节点 template 了

随后对 template 这个节点进行 挨个 nodeTransform 进行遍历,最后依旧是进入到 transformChildren

此时我们看到 template 的子节点

js 复制代码
"children": [
                {
                    "type": 1,
                    "tag": "h2",
                    "ns": 0,
                    "tagType": 0,
                    "props": [
                        {
                            "type": 7,
                            "name": "if",
                            "rawName": "v-if",
                            "exp": {
                                "type": 4,
                                "loc": {
                                    "start": {
                                        "column": 15,
                                        "line": 2,
                                        "offset": 26
                                    },
                                    "end": {
                                        "column": 19,
                                        "line": 2,
                                        "offset": 30
                                    },
                                    "source": "true"
                                },
                                "content": "true",
                                "isStatic": false,
                                "constType": 0,
                                "ast": null
                            },
                            "modifiers": [],
                            "loc": {
                                "start": {
                                    "column": 9,
                                    "line": 2,
                                    "offset": 20
                                },
                                "end": {
                                    "column": 20,
                                    "line": 2,
                                    "offset": 31
                                },
                                "source": "v-if=\"true\""
                            }
                        }
                    ],
                    "children": [
                        {
                            "type": 5,
                            "content": {
                                "type": 4,
                                "loc": {
                                    "start": {
                                        "column": 24,
                                        "line": 2,
                                        "offset": 35
                                    },
                                    "end": {
                                        "column": 29,
                                        "line": 2,
                                        "offset": 40
                                    },
                                    "source": "title"
                                },
                                "content": "title",
                                "isStatic": false,
                                "constType": 0,
                                "ast": null
                            },
                            "loc": {
                                "start": {
                                    "column": 21,
                                    "line": 2,
                                    "offset": 32
                                },
                                "end": {
                                    "column": 32,
                                    "line": 2,
                                    "offset": 43
                                },
                                "source": "{{ title }}"
                            }
                        }
                    ],
                    "loc": {
                        "start": {
                            "column": 5,
                            "line": 2,
                            "offset": 16
                        },
                        "end": {
                            "column": 37,
                            "line": 2,
                            "offset": 48
                        },
                        "source": "<h2 v-if=\"true\">{{ title }}</h2>"
                    }
                }
            ],

里面对应的 template 就是 <h2 v-if="true">{{ title }}</h2>

因此当前的 ast 就是 h2 这个 tag,可以看到下面这个调用栈关系

遍历 h2 节点时,在经过第一个 nodeTransform 函数,也就是 transformVIf,应该不出所料会返回一个 exitFn

我们来看下 transformVIf 这个能够改变 dom 结构的函数

可以看到 transformVIf 是 createStructuralDirectiveTransform 高阶函数的返回值,这个函数接受一个指令参数数组和一个 真正 处理 v-if 指令的 processIf 函数。v-for 其实也是 createStructuralDirectiveTransform 函数的返回值,只不过 v-for 传入的参数则是'for', processFor

createStructuralDirectiveTransform

ts 复制代码
export function createStructuralDirectiveTransform(
  name: string | string[],
  fn: StructuralDirectiveTransform,
): NodeTransform {
  const matches = (n: string) =>
    isString(name) ? n === name : name.includes(n)

  return (node, context) => {
    if (node.type === NodeTypes.ELEMENT) {
      const { props } = node
      // structural directive transforms are not concerned with slots
      // as they are handled separately in vSlot.ts
      if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
        return
      }
      const exitFns = []
      for (const prop of props) {
        if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
          const onExit = fn(
            node,
            prop as VaporDirectiveNode,
            context as TransformContext<ElementNode>,
          )
          if (onExit) exitFns.push(onExit)
        }
      }
      return exitFns
    }
  }
}

createStructuralDirectiveTransform 函数是用来处理像 v-if,v-for 这种结构性指令的 nodeTransform 函数

这个函数先是创建了一个 matches 匹配函数给到 return 的 nodeTransform

在 return 的 nodeTransform 中,要求 nodeTypes 类型必须是 element,然后提取出 props 属性

h2 的 props 属性为

js 复制代码
[
    {
        "type": 7,
        "name": "if",
        "rawName": "v-if",
        "exp": {
            "type": 4,
            "loc": {
                "start": {
                    "column": 15,
                    "line": 2,
                    "offset": 26
                },
                "end": {
                    "column": 19,
                    "line": 2,
                    "offset": 30
                },
                "source": "true"
            },
            "content": "true",
            "isStatic": false,
            "constType": 0,
            "ast": null
        },
        "modifiers": [],
        "loc": {
            "start": {
                "column": 9,
                "line": 2,
                "offset": 20
            },
            "end": {
                "column": 20,
                "line": 2,
                "offset": 31
            },
            "source": "v-if=\"true\""
        }
    }
]

可以看到就一个 item,并且这个 prop 的属性具有 type,name,rawName,exp,loc 等属性,遍历 props 是为了筛选出当前的 prop 是不是结构性指令,结构性指令的 nodeTypes 为 7,也就是 DIRECTIVE,以及去 matches 是否有传入 ['if', 'else', 'else-if'] 的其中一项

然后再去执行 onExit = fn(),也就是拿到 processIf 的返回函数,传入了参数 node,props,context

我们现在深入到 processIf 中去

processIf
ts 复制代码
export function processIf(
  node: ElementNode,
  dir: VaporDirectiveNode,
  context: TransformContext<ElementNode>,
): (() => void) | undefined {
  // 1. 验证表达式 v-if = "exp"
  if (dir.name !== 'else' && (!dir.exp || !dir.exp.content.trim())) {
    const loc = dir.exp ? dir.exp.loc : node.loc
    context.options.onError(
      createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc),
    )
    dir.exp = createSimpleExpression(`true`, false, loc)
  }

  // 2. 设置动态标志  
  context.dynamic.flags |= DynamicFlag.NON_TEMPLATE
  // 3. 处理 v-if 分支  
  if (dir.name === 'if') {
    const id = context.reference()
    context.dynamic.flags |= DynamicFlag.INSERT
    const [branch, onExit] = createIfBranch(node, context)

    return () => {
      onExit()
      context.dynamic.operation = {
        type: IRNodeTypes.IF,
        id,
        condition: dir.exp!,
        positive: branch,
        once:
          context.inVOnce ||
          isStaticExpression(dir.exp!, context.options.bindingMetadata),
      }
    }
  } else {
    // check the adjacent v-if
    const siblingIf = getSiblingIf(context, true)

    const siblings = context.parent && context.parent.dynamic.children
    let lastIfNode
    if (siblings) {
      let i = siblings.length
      while (i--) {
        if (
          siblings[i].operation &&
          siblings[i].operation!.type === IRNodeTypes.IF
        ) {
          lastIfNode = siblings[i].operation
          break
        }
      }
    }

    if (
      // check if v-if is the sibling node
      !siblingIf ||
      // check if IfNode is the last operation and get the root IfNode
      !lastIfNode ||
      lastIfNode.type !== IRNodeTypes.IF
    ) {
      context.options.onError(
        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
      )
      return
    }

    while (lastIfNode.negative && lastIfNode.negative.type === IRNodeTypes.IF) {
      lastIfNode = lastIfNode.negative
    }

    // Check if v-else was followed by v-else-if
    if (dir.name === 'else-if' && lastIfNode.negative) {
      context.options.onError(
        createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
      )
    }

    // TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
    if (__DEV__ && context.root.comment.length) {
      node = wrapTemplate(node, ['else-if', 'else'])
      context.node = node = extend({}, node, {
        children: [...context.comment, ...node.children],
      })
    }
    context.root.comment = []

    const [branch, onExit] = createIfBranch(node, context)

    if (dir.name === 'else') {
      lastIfNode.negative = branch
    } else {
      lastIfNode.negative = {
        type: IRNodeTypes.IF,
        id: -1,
        condition: dir.exp!,
        positive: branch,
        once: context.inVOnce,
      }
    }

    return () => onExit()
  }
}

第一步是为了验证表达式,v-if,v-else-if 需要表达式,v-else 则不用,若缺少表达式,会报错并且设置默认值 true

比如我这里的 h2 写的 v-if 为 true,那么 prop.exp.content 就是 true ,dir.exp.content.trim() 将两端的空字符去掉后还是 true,!true 就是false,自然不会进入 if 分支中

第二步是为了设置动态标志,context 身上有个 dynamic 属性,上面已经讲过了,默认值为 referenced ,其二进制 001,这里的表达式为 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE ,也就是去和 non_template 与运算

在二进制中,每个值都代表一个开关,

按位或 | 规则如下:

A(原值) B(新增标志) A | B(结果)
0001 0100 0101
0100 0100 0100
0000 0100 0100

而 non_template 的 二进制为 010,因此 001 和 010 得到的结果便是 011,也就是十进制的 3,代表这个 dynamic 属性即是 referenced,又是 non_template,翻译过来就是这个节点既被引用了又不是模板生成的

第三步就是 processIf 的核心内容了,也就是处理 v-if 分支

我这里的写的 v-if,那必然走的第一个 name 为 if 的分支,而不是 else,或者 else-if

js 复制代码
  if (dir.name === 'if') {
    // 分配唯一 id  
    const id = context.reference()
    // 标记需要插入 dom
    context.dynamic.flags |= DynamicFlag.INSERT
    const [branch, onExit] = createIfBranch(node, context)
	// exitFns
    return () => {
      onExit()
      context.dynamic.operation = {
        type: IRNodeTypes.IF,
        id,
        condition: dir.exp!,
        positive: branch,
        once:
          context.inVOnce ||
          isStaticExpression(dir.exp!, context.options.bindingMetadata),
      }
    }
  } 

先是依靠 TransformContext 身上的 方法 referenced 去创建一个 id,我们就深入 referenced 是如何实现的

dynamic 身上若有 id 则返回这个 id,没有就再次和 referenced 与运算

咱们这里没有这个 id,那就 flags 去和 referenced 与运算一下,也就是 011 和 001 去 与,那还是 011,也就是 3

这个 id 可以看到是 globalId 的++,globalId 是 TransformContext 的静态属性,默认值为 0,globalId++ 是先返回后自增,因此 id 最后就还是 0

拿到 id 为 0 后还需要去参与一次与运算, context.dynamic.flags |= DynamicFlag.INSERT,再次与运算的目的是 标记当前节点需要插入 dom,insert 的二进制 为 100,那么 011 和 100 参与一次与运算就是 111,也就是十进制 7,这里 7 其实刚好对应着 NodeTypes 的 DIRECTIVE 值,这会在后面的 wrapTemplate 函数中有体现

然后去通过 createIfBranch 去拿到 branch 和 onExit 函数

ts 复制代码
export function createIfBranch(
  node: ElementNode,
  context: TransformContext<ElementNode>,
): [BlockIRNode, () => void] {
  context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])

  const branch: BlockIRNode = newBlock(node)
  const exitBlock = context.enterBlock(branch)
  context.reference()
  return [branch, exitBlock]
}

可以看到 createIfBranch 的第一个返回值是 BlockIRNode

branch 就是 IR 的结构

随后执行 context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])

wrapTemplate

这个函数的目的就是将 当前 节点 h2 用 template 包裹起来,也就是将带有结构性指令的元素包装在一个 <template> 标签中。这个函数的第一段代码可以体现出

ts 复制代码
  if (node.tagType === ElementTypes.TEMPLATE) {
    return node
  }

ast 会有个 tagType 属性

ts 复制代码
export enum ElementTypes {
  ELEMENT,
  COMPONENT,
  SLOT,
  TEMPLATE,
}

我们这里是 element,我们再看下 wrapTemplate 的返回值部分

ts 复制代码
// 分离指令和普通属性
node.props.forEach(prop => {
    if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
      // 结构性指令留在 template 上
      reserved.push(prop)
    } else {
      // 其余属性传递给 原元素
      pass.push(prop)
    }
  })
  // 创建新的 template 节点
  return extend({}, node, {
    type: NodeTypes.ELEMENT,
    tag: 'template',
    props: reserved,
    tagType: ElementTypes.TEMPLATE,
    children: [extend({}, node, { props: pass } as TemplateChildNode)], // 原元素作为子节点,包含其他属性
  } as Partial<TemplateNode>)

此时 prop.type 为 两次与运算的结果 7,刚好符合 NodeTypes 的 DIRECTIVE 值,因此会执行 reserved.push(prop),reserved 是个空数字,数据类型为 AttributeNode 或 DirectiveNode 类型

wrapTemplate 返回值将当前的 ast 打上了 tag 为 template 以及 tagType 为 template 的标记,并且把 原来的元素放到了 Children 中

因此 wrapTemplate 我们可以这样理解,h2 在转化前为

vue 复制代码
<h2 v-if="true">{{ title }}</h2>

wrapTemplate 后为

vue 复制代码
<template v-if="true">
	<h2>{{ title }}</h2>
</template>

这么做的目的就是将元素和指令分离,比如 h2 有可能有 class 或者绑定事件,那 h2 v-if false 消失时,整个 h2 都不存在了,但是 h2 的属性仍然需要在 条件为 true 时发挥作用。分离后 template 就只负责 控制整个块的显隐,里面的 标签会一直保留属性

回到 createIfBranch 中,现在是为了拿到 branch,执行 const branch: BlockIRNode = newBlock(node) ,newBlock 初始化了 block 的结构,block 里面依旧有一个 dynamic.flag 为 referenced 的 默认值

然后拿到 exitFn,这个函数是通过 context.enterBlock 拿到的

ts 复制代码
  enterBlock(ir: BlockIRNode, isVFor: boolean = false): () => void {
    const { block, template, dynamic, childrenTemplate, slots } = this
    this.block = ir
    this.dynamic = ir.dynamic
    this.template = ''
    this.childrenTemplate = []
    this.slots = []
    isVFor && this.inVFor++
    return () => {
      // exit
      this.registerTemplate()
      this.block = block
      this.template = template
      this.dynamic = dynamic
      this.childrenTemplate = childrenTemplate
      this.slots = slots
      isVFor && this.inVFor--
    }
  }

可以看到这个函数就是返回了一个对象,里面包含了 block,也就是 ir ,以及其他属性,还有个 registerTemplate 函数

在返回 branch 和 onExit 给到 processIf 前,还会执行一次 context.referenced,此时 dynamic.id 就更新为 1

回到 processIf ,可以看到 onExit 为

ts 复制代码
() => {
      onExit()
      context.dynamic.operation = {
        type: IRNodeTypes.IF,
        id,
        condition: dir.exp!,
        positive: branch,
        once:
          context.inVOnce ||
          isStaticExpression(dir.exp!, context.options.bindingMetadata),
      }
    }

至此,第一个收集到的 exitFns 即是 transformVIf 给到的

这里说第一个收集指的是在 h2 这一层,其实在 root,以及 template 层都有收集到 transformElement 给到的 exitFns

随后针对当前 h2 这个 ast 继续收集 exitFns,也就是遍历 nodeTransforms,会走到 transformElement 这个函数

transformElement 会返回一个 postTransformElement 函数,这个函数放到 exitFns 时一起讲

收集后,最后一次遍历到 transformChildren,这个时候你肯定会误以为<h2 v-if="true">{{ title }}</h2> h2 的 子节点 就是 {{ title }} 插值,是也不是

因为 wrapTemplate 后,ast 已经变了,原本 h2 当前节点就是 h2,wrapTemplate 后,当前节点变成了 template ,子节点变成了 h2,因此又需要执行一次 transformChildren

这也就是为啥在 transformNode 中有一个 else 分支

ts 复制代码
else {
  // node may have been replaced
  node = context.node
}

node 节点可能会被 v-if 改变

现在回到 processIf 后的 ast,tag 已经变成了 template,同级的 props 带有 v-if 等 rawName 信息,而 template 的子节点 h2 的props 变成了空数组,因为在 wrapTemplate 阶段已经被 pass 替换了

因此再次执行到 h2 层 的 ast 时,遍历到 transformVIf 是不会返回一个 exitFns 的,因此这里会直接 transformChildren 来到 {{ title }}

{{ ttile }} 的 ast 如下

js 复制代码
{
    "type": 5,
    "content": {
        "type": 4,
        "loc": {
            "start": {
                "column": 24,
                "line": 2,
                "offset": 35
            },
            "end": {
                "column": 29,
                "line": 2,
                "offset": 40
            },
            "source": "title"
        },
        "content": "title",
        "isStatic": false,
        "constType": 0,
        "ast": null
    },
    "loc": {
        "start": {
            "column": 21,
            "line": 2,
            "offset": 32
        },
        "end": {
            "column": 32,
            "line": 2,
            "offset": 43
        },
        "source": "{{ title }}"
    }
}

NodeTypes 为 5 对应着 NodeTypes.INTERPOLATION ,也就是插值

然后在这一层从外到内收集 exifFns 完毕

执行 exitFns

exitFns 是各个 nodeTransform 函数 return 的函数,在所有子节点处理完成执行后,负责生成元素的最终 IR 操作

现在开始从内到外执行 exitFns

我们可以先看下当前的递归调用栈,理解嵌套关系

现在是处于最里层,只有一个 transformElement 执行返回的 postTransformElement 函数

postTransformElement

ts 复制代码
  return function postTransformElement() {
 	// 1. 节点类型验证
    ;({ node } = context)
    if (
      !(
        node.type === NodeTypes.ELEMENT &&
        (node.tagType === ElementTypes.ELEMENT ||
          node.tagType === ElementTypes.COMPONENT)
      )
    )
      return
	// 2. 构建属性信息
    const isComponent = node.tagType === ElementTypes.COMPONENT
    const isDynamicComponent = isComponentTag(node.tag)
    const propsResult = buildProps(
      node,
      context as TransformContext<ElementNode>,
      isComponent,
      isDynamicComponent,
      getEffectIndex,
    )
	// 3. 找到真正的父节点
    let { parent } = context
    while (
      parent &&
      parent.parent &&
      parent.node.type === NodeTypes.ELEMENT &&
      parent.node.tagType === ElementTypes.TEMPLATE
    ) {
      parent = parent.parent
    }
    // 4. 判断单根节点
    const singleRoot =
      context.root === parent &&
      parent.node.children.filter(child => child.type !== NodeTypes.COMMENT)
        .length === 1
	// 5. 分发到具体的转换函数
    if (isComponent) {
      transformComponentElement(
        node as ComponentNode,
        propsResult,
        singleRoot,
        context,
        isDynamicComponent,
      )
    } else {
      transformNativeElement(
        node as PlainElementNode,
        propsResult,
        singleRoot,
        context,
        getEffectIndex,
      )
    }
  }

像是 {{ title }} 这一层的 ast ,执行第一步就被 return 出来了,因为没有 node.type 属性

现在回到 transformChildren 后半部分,也就是 transformNode(childContext) 之后

ts 复制代码
  for (const [i, child] of node.children.entries()) {
    const childContext = context.create(child, i)
    transformNode(childContext)
	// 退出部分
    const childDynamic = childContext.dynamic

    if (isFragment) {
      childContext.reference()
      childContext.registerTemplate()

      if (
        !(childDynamic.flags & DynamicFlag.NON_TEMPLATE) ||
        childDynamic.flags & DynamicFlag.INSERT
      ) {
        context.block.returns.push(childContext.dynamic.id!)
      }
    } else {
      context.childrenTemplate.push(childContext.template)
    }
	// 收集返回值
    if (
      childDynamic.hasDynamicChild ||
      childDynamic.id !== undefined ||
      childDynamic.flags & DynamicFlag.NON_TEMPLATE ||
      childDynamic.flags & DynamicFlag.INSERT
    ) {
      context.dynamic.hasDynamicChild = true
    }

    context.dynamic.children[i] = childDynamic
  }
  // 普通节点
  if (!isFragment) {
    processDynamicChildren(context as TransformContext<ElementNode>)
  }

当前的 ast 已经从 {{ title }} 退到了 h2 标签,由于不是 isFragment,直接走到了收集返回值部分,由于 flags 为 011,non_template 就是 010,因此这里会进入 if 分支内去标记 拥有动态子节点 hasDynamicChild,并再将 {{ title }} 动态信息赋值给到 h2 的 dynamic.children 属性

随后针对 普通节点 解决动态节点的插入位置问题,执行 processDynamicChildren

ts 复制代码
function processDynamicChildren(context: TransformContext<ElementNode>) {
  let prevDynamics: IRDynamicInfo[] = [] // 累积的连续动态节点
  let hasStaticTemplate = false // 静态模板节点
  const children = context.dynamic.children // 子节点的动态信息

  // 收集需要插入的动态节点
  for (const [index, child] of children.entries()) {
    if (child.flags & DynamicFlag.INSERT) {
      prevDynamics.push(child)
    }
	// 遇到静态节点时处理之前的动态节点
    if (!(child.flags & DynamicFlag.NON_TEMPLATE)) {
      if (prevDynamics.length) {
        if (hasStaticTemplate) {
          context.childrenTemplate[index - prevDynamics.length] = `<!>`
          prevDynamics[0].flags -= DynamicFlag.NON_TEMPLATE
          const anchor = (prevDynamics[0].anchor = context.increaseId())
          registerInsertion(prevDynamics, context, anchor)
        } else {
          registerInsertion(prevDynamics, context, -1 /* prepend */)
        }
        prevDynamics = []
      }
      hasStaticTemplate = true
    }
  }
  // 没有静态模板情况
  if (prevDynamics.length) {
    registerInsertion(prevDynamics, context)
  }
}

processDynamicChildren 会解决动态节点的插入位置,它将需要动态插入的节点分组,并为每组节点确定正确的插入位置

当有静态模板的情况会插入锚点 <!----> 作为位置标记

对于 h2 标签来讲,并没有需要插入的操作,因此这个函数并不会去执行

因此会去执行 postTransformElement 退出函数

h2 会在这里拿到真正的父节点 template ,而不是自己生成的 template ,随后会去执行 transformNativeElement 函数

这个函数负责将原生的 html 标签生成对应的 模板字符,和动态操作 IR

现在回退到了 template(v-if),这里面有两个 exitFns

在 transformChildren 后半部分,由于是 template,会被判定为 Fragment,会进去执行 childContext.reference()childContext.registerTemplate()

reference 是确定自己 dynamic.id,registerTemplate 是将上一步得到的 template ,也就是 <h2></h2> push 到 ir.template 中

ts 复制代码
  pushTemplate(content: string): number {
    const existing = this.ir.template.findIndex(
      template => template === content,
    )
    if (existing !== -1) return existing
    this.ir.template.push(content)
    return this.ir.template.length - 1
  }

在执行 exitFns,会先执行 postTransformElement,后执行 processIf 返回的 退出函数

对于 template(v-if) 来讲,执行 postTransformElement 会被直接 return,以为 不是普通节点,这是 template 节点,因此现在执行 processIf 返回的 退出函数

js 复制代码
() => {
      onExit()
      context.dynamic.operation = {
        type: IRNodeTypes.IF,
        id,
        condition: dir.exp!,
        positive: branch,
        once:
          context.inVOnce ||
          isStaticExpression(dir.exp!, context.options.bindingMetadata),
      }
    }
  }

onExit() 是 entertBlock 返回的函数

js 复制代码
    return () => {
      // exit
      this.registerTemplate()
      this.block = block
      this.template = template
      this.dynamic = dynamic
      this.childrenTemplate = childrenTemplate
      this.slots = slots
      isVFor && this.inVFor--
    }

当然我这里 v-if 写死的 true,在执行 isStaticExpression 会步入到 响应式 的逻辑,这部分内容需要单独来讲,这里不做讨论

后续的 template 以及 root 的执行都是重复的逻辑

最后得到 ir 如下

为啥针对每个 ast 节点用 nodeTransforms 遍历时,得到了 exitFns 要去倒着执行?

从外到内收集 exitFns 是为了分析节点结构,设置状态,但此时不生成 ir,等子节点处理完,比如这个过程 v-if 的节点结构会发生改变

从内到位执行 exitFns 是为了保证子节点先完成,父节点后完成,这是因为父节点的 ir 依赖子节点处理。这个过程也就是构建 IR 的过程

总结

将 ast 给到 transform 可以得到 ir,transform 的核心是有两个阶段,第一阶段是收集 exitFns,这里是深度优先遍历,针对每个节点去挨个遍历 nodeTransforms,也就是结构性指令,这个阶段是为了服务第二个阶段的执行 exitFns,也就是会分析 ast 的结构信息,并设置一些标志,比如 processIf 里面有个 wrapTemplate 就会重新组建立结构,但是又能保证与父节点的关系。

第二个阶段是执行 exitFns 会去生成 ir,比如处理动态子节点,处理插入位置,静态节点会直接写入 html 模板,动态属性则是生成运行时的操作信息,比如 dynamic.operation

两个阶段合在一起就是声明式 ast 到 命令式 ir 的转变

相关推荐
爷_2 小时前
字节跳动震撼开源Coze平台!手把手教你本地搭建AI智能体开发环境
前端·人工智能·后端
charlee444 小时前
行业思考:不是前端不行,是只会前端不行
前端·ai
Amodoro5 小时前
nuxt更改页面渲染的html,去除自定义属性、
前端·html·nuxt3·nuxt2·nuxtjs
Wcowin5 小时前
Mkdocs相关插件推荐(原创+合作)
前端·mkdocs
伍哥的传说6 小时前
CSS+JavaScript 禁用浏览器复制功能的几种方法
前端·javascript·css·vue.js·vue·css3·禁用浏览器复制
lichenyang4536 小时前
Axios封装以及添加拦截器
前端·javascript·react.js·typescript
Trust yourself2436 小时前
想把一个easyui的表格<th>改成下拉怎么做
前端·深度学习·easyui
苹果醋36 小时前
iview中实现点击表格单元格完成编辑和查看(span和input切换)
运维·vue.js·spring boot·nginx·课程设计
武昌库里写JAVA6 小时前
iView Table组件二次封装
vue.js·spring boot·毕业设计·layui·课程设计
三口吃掉你6 小时前
Web服务器(Tomcat、项目部署)
服务器·前端·tomcat