上一篇文章【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 的转变