上篇文章中我们介绍了 Vue3 将 template 解析成 ast 的过程,得到 ast 后,我们就可以对 ast 上的节点和属性进行相应的操作,也就是之前我们看过的 transform 的过程。
1.transform
// 遍历 AST 节点树,对上面生成的 AST 进行指令转换,生成可用节点,同时根据 compiler
// 传入的配置(如是否做静态节点提升等)对 AST 节点树进行优化处理,为 rootNode 及
// 下属每个节点挂载 codegenNode
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
我们来详细看一下 transform 方法。
/**
{
type: NodeTypes.SIMPLE_EXPRESSION, // 表达式类型标识
loc, // 位置信息
isConstant, // 是否是常量
content, // 表达式内容
// 是否是静态的,
// e.g. v-bind:attr="value",value如果是动态变化的变量
// v-bind:attr="true",true是常量不会变化,因此是静态的
isStatic
}
// 比如v-bind:attr="true",true转换为简单表达式对象就是
// { isContant: true, content: 'true', isStatic: true ... }
*/
// transform 函数对静态提升其决定性作用的两件事:
// 1. 将原始 AST 中的静态节点对应的 AST Element 赋值给根 AST 的 hoists 属性。
// 2. 获取原始 AST 需要的 helpers 对应的键名,用于 generate 阶段的生成可执行代码的获取对应函数,
// 例如 createTextVNode、createStaticVNode、renderList 等等。
// 并且,在 traverseNode 函数中会对 AST Element 应用具体的 transform 函数,大致可以分为两类:
// 1. 静态节点 transform 应用,即节点不含有插值、指令、props、动态样式的绑定等。
// 2. 动态节点 transform 应用,即节点含有插值、指令、props、动态样式的绑定等。
// `<div>hi vue3</div>` 会命中 transformElement 和 transformText 两个 plugin 的逻辑。
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers.keys()]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = context.imports
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
其中最核心的步骤是 traverseNode :
// 遍历AST节点树过程中,通过node转换器(nodeTransforms)对当前节点进行node转换,
// 子节点全部遍历完成后执行对应指令的onExit回调退出转换。对v-if、v-for等指令的转换生成对应节点,
// 都是由nodeTransforms中对应的指令转换工具完成的。
// 经nodeTransforms处理过的AST节点会被挂载codeGenNode属性(其实就是调用vnode创建的interface),
// 该属性包含patchFlag等在AST解析阶段无法获得的信息,其作用就是为了在后面的generate阶段生成vnode的创建调用。
// 本质上codegenNode是一个表达式对象。
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
for (let i = 0; i < nodeTransforms.length; i++) {
// 依次执行 nodeTransforms
// Transform 会返回一个退出函数,在处理完所有的子节点后再执行
// 其中的包含我们调用 compile 的时候传入的 options.nodeTransforms
// 转换器:transformElement、transformExpression、transformText、
// transformElement负责整个节点层面的转换,
// transformExpression负责节点中表达式的转化,
// transformText负责节点中文本的转换,转换后会增加一堆表达式表述对象
const onExit = nodeTransforms[i](node, context)
if (onExit) {
if (isArray(onExit)) {
exitFns.push(...onExit)
} else {
exitFns.push(onExit)
}
}
if (!context.currentNode) {
// node was removed
return
} else {
// node may have been replaced
node = context.currentNode
}
}
switch (node.type) {
case NodeTypes.COMMENT:
// 处理注释节点
if (!context.ssr) {
// inject import for the Comment symbol, which is needed for creating
// comment nodes with `createVNode`
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// 处理插值表达式节点
// no need to traverse, but we need to inject toString helper
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.IF:
// 处理 if 表达式节点
for (let i = 0; i < node.branches.length; i++) {
traverseNode(node.branches[i], context)
}
break
case NodeTypes.IF_BRANCH:
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
// exit transforms
context.currentNode = node
let i = exitFns.length
// 执行所有 Transform 的退出函数
while (i--) {
exitFns[i]()
}
}
在上一篇模板编译里面我们曾讲过,默认的 nodeTransforms 包括目标节点结构的转换插件、指令转换插件两大类, 我们可以看一下之前提到的这两类中的 transformIf 插件。
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
/**
在 transformIf 方法体内我们可以通过操作 node 来实现一些自定义的节点、属性的转换规则
nodeTransform 方法的返回值就是退出函数,退出函数会在退出 traverseNode 方法前被依次执行
while (i--) {
exitFns[i]()
}
*/
// processIf 会在退出 traverseNode 方法前被执行
// 进行 if 节点的处理,为其标记相应的 codegenNode
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// #1587: We need to dynamically increment the key based on the current
// node's sibling nodes, since chained v-if/else branches are
// rendered at the same depth
const siblings = context.parent!.children
let i = siblings.indexOf(ifNode)
let key = 0
while (i-- >= 0) {
const sibling = siblings[i]
if (sibling && sibling.type === NodeTypes.IF) {
key += sibling.branches.length
}
}
// Exit callback. Complete the codegenNode when all children have been
// transformed.
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
我们参考 transformIf 方法,就可以实现自定义的 transform 方法,来对 Vnode 和 attrs 进行一些特殊操作了。
看完 transform ,接下来就是 generate 阶段了。generate 阶段的主要过程就是将 transform 转换后的 AST 生成对应的可执行代码,从而在之后 Runtime 的 Render 阶段时,
就可以通过可执行代码生成对应的 VNode Tree,然后最终在页面上映射为真实的 DOM Tree 。
TODO: export function generate
TODO:走完亲戚继续写
Vue3 源码解读
- Vue3 源码解读(1)—— 环境搭建
- Vue3 源码解读(2)—— 初始化过程
- Vue3 源码解读(3)—— 挂载根节点
- Vue3 源码解读(4)—— 模板编译
- Vue3 源码解读(5)—— transform与generate
参考链接: