序
在了解了编译器的parse阶段后,继续学习编译过程中的转换(Tranform)阶段
本文以模板源码如下为例展开对应的知识学习:
<div id="app">
<button @click="insert">insert at random index</button>
<span>{{msg}} 1111</span>
<span class="static">{{name + msg}}</span>
<p>普通静态文本</p >
<ul>
<li v-for="item in items" :key="item">{{item}}</li>
</ul>
<span v-if="show">showsshow</span>
<span v-else>show==false</span>
</div>
</div>
通过parse阶段创建AST的结果内容为记录了节点的标签名,内容,类型,属性,闭合状态,修饰符 以及节点在源 HTML 字符串中的位置,包含行,列,偏移量等信息....大致树结构如下图所示:
transform
AST对象是对模板的完整描述,但是它还不能直接拿来生成代码,因为它的语义化还不够,没有包含和编译优化的相关属性,在 transform 阶段,Vue 会对 AST 进行一些转换操作,主要是根据不同的 AST 节点添加不同的编译优化选项参数先通过 getBaseTransformPreset 方法获取节点和指令转换的内置方法,然后调用 transform 方法做 AST 转换,并且把这些节点和指令的转换方法作为配置的属性参数传入
function getBaseTransformPreset(prefixIdentifiers) {
return [
[
transformOnce,
transformIf,
transformFor,
transformExpression,
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
创建 transform 上下文
首先,我们来看创建 transform 上下文的过程,其实和 parse 过程一样,在 transform 阶段会创建一个上下文对象,它的实现的结果:
这个上下文对象 context 维护了 transform 过程的一些配置,比如前面提到的节点和指令的转换函数等;还维护了 transform 过程的一些状态数据,比如当前处理的 AST 节点,当前 AST 节点在子节点中的索引,以及当前 AST 节点的父节点等。此外,context 还包含了在转换过程中可能会调用的一些辅助函数,和一些修改 context 对象的方法。
遍历 AST 节点
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]()
}
}
几个内置转换方法解析
transformElement() 元素节点转换
主体代码
判断节点是不是一个 Block 节点
为了运行时的更新优化,Vue.js 3.0 设计了一个 Block tree 的概念。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,极大优化了 diff 的效率,模板的动静比越大,这个优化就会越明显。 因此在编译阶段,我们需要找出哪些节点可以构成一个 Block,其中动态组件、svg、foreignObject 标签以及动态绑定的 prop 的节点都被视作一个 Block。
// 动态组件、svg、foreignObject 标签以及动态绑定 key prop 的节点都被视作一个 Block
let shouldUseBlock =
// dynamic component may resolve to plain elements
isDynamicComponent ||
vnodeTag === TELEPORT ||
vnodeTag === SUSPENSE ||
(!isComponent &&
// <svg> and <foreignObject> must be forced into blocks so that block
// updates inside get proper isSVG flag at runtime. (#639, #643)
// This is technically web-specific, but splitting the logic out of core
// leads to too much unnecessary complexity.
(tag === 'svg' || tag === 'foreignObject'))
// 把 KeepAlive 看做是一个 Block,这样可以避免它的子节点的动态节点被父 Block 收集
if (vnodeTag === KEEP_ALIVE) {
// Although a built-in component, we compile KeepAlive with raw children
// instead of slot functions so that it can be used inside Transition
// or other Transition-wrapping HOCs.
// To ensure correct updates with block optimizations, we need to:
// 1. Force keep-alive into a block. This avoids its children being
// collected by a parent block.
shouldUseBlock = true
// 2. Force keep-alive to always be updated, since it uses raw children.
// 2. 确保它始终更新
patchFlag |= PatchFlags.DYNAMIC_SLOTS
if (__DEV__ && node.children.length > 1) {
context.onError(
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
})
)
}
处理节点的 props
这个过程主要是从 AST 节点的 props对象中进一步解析出指令 vnodeDirectives、动态属性 dynamicPropNames,以及更新标识 patchFlag。patchFlag 主要用于标识节点更新的类型,在组件更新的优化中会用到。
// 处理 props
if (props.length > 0) {
const propsBuildResult = buildProps(
node,
context,
undefined,
isComponent,
isDynamicComponent
)
vnodeProps = propsBuildResult.props
patchFlag = propsBuildResult.patchFlag
dynamicPropNames = propsBuildResult.dynamicPropNames
const directives = propsBuildResult.directives
vnodeDirectives =
directives && directives.length
? (createArrayExpression(
directives.map(dir => buildDirectiveArgs(dir, context))
) as DirectiveArguments)
: undefined
if (propsBuildResult.shouldUseBlock) {
shouldUseBlock = true
}
}
处理节点的 children
// 处理 children
if (node.children.length > 0) {
// 把 KeepAlive 看做是一个 Block,这样可以避免它的子节点的动态节点被父 Block 收集
if (vnodeTag === KEEP_ALIVE) {
// Although a built-in component, we compile KeepAlive with raw children
// instead of slot functions so that it can be used inside Transition
// or other Transition-wrapping HOCs.
// To ensure correct updates with block optimizations, we need to:
// 1. Force keep-alive into a block. This avoids its children being
// collected by a parent block.
shouldUseBlock = true
// 2. Force keep-alive to always be updated, since it uses raw children.
// 2. 确保它始终更新
patchFlag |= PatchFlags.DYNAMIC_SLOTS
if (__DEV__ && node.children.length > 1) {
context.onError(
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
})
)
}
}
const shouldBuildAsSlots =
isComponent &&
// / Teleport不是一个真正的组件,它有专门的运行时处理
vnodeTag !== TELEPORT &&
// explained above.
vnodeTag !== KEEP_ALIVE
if (shouldBuildAsSlots) {
// 组件有 children,则处理插槽
const { slots, hasDynamicSlots } = buildSlots(node, context)
vnodeChildren = slots
if (hasDynamicSlots) {
patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
} else if (node.children.length === 1 && vnodeTag !== TELEPORT) {
const child = node.children[0]
const type = child.type
// check for dynamic text children
const hasDynamicTextChild =
type === NodeTypes.INTERPOLATION ||
type === NodeTypes.COMPOUND_EXPRESSION
// 如果只是一个普通文本节点、插值或者表达式,直接把节点赋值给 vnodeChildren
if (
hasDynamicTextChild &&
getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
patchFlag |= PatchFlags.TEXT
}
// pass directly if the only child is a text node
// (plain / interpolation / expression)
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
vnodeChildren = child as TemplateTextChildNode
} else {
vnodeChildren = node.children
}
} else {
vnodeChildren = node.children
}
}
对于一个组件节点而言,如果它有子节点,则说明是组件的插槽,另外还会有对一些内置组件比如 KeepAlive、Teleport 的处理逻辑。
对于一个普通元素节点,我们通常直接拿节点的 children 属性给 vnodeChildren 即可,但有一种特殊情况,如果节点只有一个子节点,并且是一个普通文本节点、插值或者表达式,那么直接把节点赋值给 vnodeChildren。
处理 patchFlag 和 dynamicPropNames
对props解析结果patchFlag 和 dynamicPropNames 做进一步处理
// 处理 patchFlag 和 dynamicPropNames
if (patchFlag !== 0) {
if (__DEV__) {
if (patchFlag < 0) {
// special flags (negative and mutually exclusive)
vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
} else {
// 获取 flag 对应的名字,生成注释,方便理解生成代码对应节点的 pathFlag
const flagNames = Object.keys(PatchFlagNames)
.map(Number)
.filter(n => n > 0 && patchFlag & n)
.map(n => PatchFlagNames[n])
.join(`, `)
vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
}
} else {
vnodePatchFlag = String(patchFlag)
}
if (dynamicPropNames && dynamicPropNames.length) {
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
}
通过过 createVNodeCall 创建了实现 VNodeCall 接口的代码生成节点
codegenNode 相比之前的 AST 节点对象,多了很多和编译优化相关的属性,它们会在代码生成阶段会起到非常重要作用
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
isComponent,
node.loc
)
export function createVNodeCall(
context: TransformContext | null,
tag: VNodeCall['tag'],
props?: VNodeCall['props'],
children?: VNodeCall['children'],
patchFlag?: VNodeCall['patchFlag'],
dynamicProps?: VNodeCall['dynamicProps'],
directives?: VNodeCall['directives'],
isBlock: VNodeCall['isBlock'] = false,
disableTracking: VNodeCall['disableTracking'] = false,
isComponent: VNodeCall['isComponent'] = false,
loc = locStub
): VNodeCall {
if (context) {
if (isBlock) {
context.helper(OPEN_BLOCK)
context.helper(getVNodeBlockHelper(context.inSSR, isComponent))
} else {
context.helper(getVNodeHelper(context.inSSR, isComponent))
}
if (directives) {
context.helper(WITH_DIRECTIVES)
}
}
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent,
loc
}
}
它最后返回了一个对象,包含了传入的参数数据。这里要注意 context.helper 函数的调用,它会把一些 Symbol 对象添加到 context.helpers 数组中,目的是为了后续代码生成阶段,生成一些辅助代码
表达式转换transformExpression()
transformExpression对插值表达式,元素指令动态表达式,插槽插值表达式,过滤掉v-on 和 v-for ,因为它们都有各自的处理逻辑
举个例子,比如这个模板:{{ name + msg }} 经过 processExpression 处理后,node.content 的值变成了一个复合表达式对象:
"version": "3.2.40"结果依旧为{{name + msg}}?
transformText()文本转换
transformText 函数只处理根节点、元素节点、 v-for 以及 v-if 分支相关的节点,它也会返回一个退出函数,因为 transformText 要保证所有表达式节点都已经被处理才执行转换逻辑。
transformText 主要的目的就是合并一些相邻的文本节点,然后为内部每一个文本节点创建一个代码生成节点。
<span>{{msg}} 1111</span>两个文本节点
合并成一个复合表达式节点
transformIf()if判断转换
遍历过程中遇见v-if 代码块的时候创建 IF 节点分支给主分支生成 codegenNode;后续继续遇到条件语句把元素节点移至IF分支中将条件分支的 codegenNode 附加到 上一个条件节点的 codegenNode 的 alternate 中。
// 退出回调函数,当所有子节点转换完成执行
return () => {
// v-if 节点的退出函数
// 创建 IF 节点的 codegenNode
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// v-else-if、v-else 节点的退出函数
// 若出现其他条件分支
// 将此分支的 codegenNode 附加到 上一个条件节点的 codegenNode 的 alternate 中
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
v-if的转换结果如图:
transformFor() 方法 转换v-for
v-for方法转换后的children 有个parseResult里面存放对应的‘源’和‘值关键字’
hoistStatic静态提升
hoistStatic 主要就是从根节点开始,通过递归的方式去遍历节点,只有普通元素和文本节点才能被静态提升,所以针对这些节点,这里通过 getStaticType(getConstantType) 去获取静态类型,如果节点是一个元素类型,getStaticType 内部还会递归判断它的子节点的静态类型。
虽然有的节点包含一些动态子节点,但它本身的静态属性还是可以被静态提升的。
child.codegenNode = context.hoist(child.codegenNode)
改动后的 codegenNode 会在生成代码阶段帮助我们生成静态提升的相关代码
上述例子的静态提升的内容如下:
createRootCodegen 生成根节点
完成静态提升后,我们来到了 AST 转换的最后一步,即创建根节点的代码生成节点
function createRootCodegen(root, context) {
const { helper } = context;
const { children } = root;
const child = children[0];
if (children.length === 1) {
// 如果子节点是单个元素节点,则将其转换成一个 block
if (isSingleElementRoot(root, child) && child.codegenNode) {
const codegenNode = child.codegenNode;
if (codegenNode.type === 13 /* VNODE_CALL */) {
codegenNode.isBlock = true;
helper(OPEN_BLOCK);
helper(CREATE_BLOCK);
}
root.codegenNode = codegenNode;
}
else {
root.codegenNode = child;
}
}
else if (children.length > 1) {
// 如果子节点是多个节点,则返回一个 fragement 的代码生成节点
root.codegenNode = createVNodeCall(context, helper(FRAGMENT), undefined, root.children, `${64 /* STABLE_FRAGMENT */} /* ${PatchFlagNames[64 /* STABLE_FRAGMENT */]} */`, undefined, undefined, true);
}
}
createRootCodegen 为 root 这个虚拟的 AST 根节点创建一个代码生成节点,如果 root 的子节点 children 是单个元素节点,则将其转换成一个 Block,把这个 child 的 codegenNode 赋值给 root 的 codegenNode。
如果 root 的子节点 children 是多个节点,则返回一个 fragement 的代码生成节点,并赋值给 root 的 codegenNode。
至此 创建 codegenNode 就是为了后续生成代码时使用
createRootCodegen 完成之后,接着把 transform 上下文在转换 AST 节点过程中创建的一些变量赋值给 root 节点对应的属性,在这里可以看一下这些属性
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached