vue3源码学习11-编译two-AST内部转换
在编译one中,template经过解析最终返回了一个AST节点对象,它是对模板的完整描述,但是还是不能用于生成代码,因为语义化还不够,没有包含编译优化相关的属性,vue内部还进行了进一步的转化,来看实现:
// packages/compiler-dom/src/index.ts
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 上节看的模板生成AST
const ast = isString(template) ? baseParse(template, options) : template
// 本节主要关注的,AST内部转化
// 获取节点和指令转换的方法
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(prefixIdentifiers)
// AST 转换
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms 用户自定义transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms 用户自定义transforms
)
})
)
// 下节需要看的生成代码部分
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
先看getBaseTransformPreset返回了哪些节点和指令的转换方法:
packages/compiler-core/src/compile.ts
// 先不关注每个转换方法的实现,知道有哪些转换就行
export function getBaseTransformPreset(
prefixIdentifiers?: boolean
): TransformPreset {
return [
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
? [
// order is important
trackVForSlotScopes,
transformExpression
]
: __BROWSER__ && __DEV__
? [transformExpression]
: []),
transformSlotOutlet,
transformElement,
trackSlotScopes,
transformText
],
{
on: transformOn,
bind: transformBind,
model: transformModel
}
]
}
接着看transform函数的实现:
packages/compiler-core/src/transform.ts
export function transform(root: RootNode, options: TransformOptions) {
// 创建transform上下文
const context = createTransformContext(root, options)
// 遍历AST节点
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
if (__COMPAT__) {
root.filters = [...context.filters!]
}
}
看看创建transform上下文的过程,和parse过程类似:
export function createTransformContext(
root: RootNode,
{
filename = '',
prefixIdentifiers = false,
hoistStatic = false,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
isCustomElement = NOOP,
expressionPlugins = [],
scopeId = null,
slotted = true,
ssr = false,
inSSR = false,
ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ,
inline = false,
isTS = false,
onError = defaultOnError,
onWarn = defaultOnWarn,
compatConfig
}: TransformOptions
): TransformContext {
const nameMatch = filename.replace(/\?.*$/, '').match(/([^/\\]+)\.\w+$/)
// 这个上下文对象 context 维护了 transform 过程的一些配置,比如前面提到的节点和指令的转换函数等;
// 还维护了 transform 过程的一些状态数据,如当前处理的 AST 节点,当前 AST 节点在子节点中的索引,及当前 AST 节点的父节点等。
// 此外,context 还包含了在转换过程中可能会调用的一些辅助函数,和一些修改 context 对象的方法。
const context: TransformContext = {
// 配置
// options
selfName: nameMatch && capitalize(camelize(nameMatch[1])),
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
isCustomElement,
expressionPlugins,
scopeId,
slotted,
ssr,
inSSR,
ssrCssVars,
bindingMetadata,
inline,
isTS,
onError,
onWarn,
compatConfig,
// state
// 状态数据
root,
helpers: new Map(),
components: new Set(),
directives: new Set(),
hoists: [],
imports: [],
constantCache: new Map(),
temps: 0,
cached: 0,
identifiers: Object.create(null),
scopes: {
vFor: 0,
vSlot: 0,
vPre: 0,
vOnce: 0
},
parent: null,
currentNode: root,
childIndex: 0,
inVOnce: false,
// methods
helper(name) {
const count = context.helpers.get(name) || 0
context.helpers.set(name, count + 1)
return name
},
removeHelper(name) {
const count = context.helpers.get(name)
if (count) {
const currentCount = count - 1
if (!currentCount) {
context.helpers.delete(name)
} else {
context.helpers.set(name, currentCount)
}
}
},
helperString(name) {
return `_${helperNameMap[context.helper(name)]}`
},
replaceNode(node) {
/* istanbul ignore if */
if (__DEV__) {
if (!context.currentNode) {
throw new Error(`Node being replaced is already removed.`)
}
if (!context.parent) {
throw new Error(`Cannot replace root node.`)
}
}
context.parent!.children[context.childIndex] = context.currentNode = node
},
removeNode(node) {
if (__DEV__ && !context.parent) {
throw new Error(`Cannot remove root node.`)
}
const list = context.parent!.children
const removalIndex = node
? list.indexOf(node)
: context.currentNode
? context.childIndex
: -1
/* istanbul ignore if */
if (__DEV__ && removalIndex < 0) {
throw new Error(`node being removed is not a child of current parent`)
}
if (!node || node === context.currentNode) {
// current node removed
// 移除当前节点
context.currentNode = null
context.onNodeRemoved()
} else {
// sibling node removed
// 移除兄弟节点
if (context.childIndex > removalIndex) {
context.childIndex--
context.onNodeRemoved()
}
}
// 移除节点
context.parent!.children.splice(removalIndex, 1)
},
onNodeRemoved: () => {},
addIdentifiers(exp) {
// identifier tracking only happens in non-browser builds.
},
removeIdentifiers(exp) {
},
hoist(exp) {
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_HOIST
)
identifier.hoisted = exp
return identifier
},
cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode)
}
}
function addId(id: string) {
const { identifiers } = context
if (identifiers[id] === undefined) {
identifiers[id] = 0
}
identifiers[id]!++
}
function removeId(id: string) {
context.identifiers[id]!--
}
return context
}
创建上下文对象后,开始遍历AST节点:
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++) {
// 有些节点会设计一个退出函数,在处理完子节点后执行
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`
// 需要导入 createComment 辅助函数
context.helper(CREATE_COMMENT)
}
break
case NodeTypes.INTERPOLATION:
// no need to traverse, but we need to inject toString helper
// 需要导入 toString 辅助函数
if (!context.ssr) {
context.helper(TO_DISPLAY_STRING)
}
break
// for container types, further traverse downwards
case NodeTypes.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
while (i--) {
exitFns[i]()
}
}
现在来看看vue内置的八种转换函数中的四种:
1.元素节点转换:
// packages/compiler-core/src/transforms/transformElement.ts
// generate a JavaScript AST for this element's codegen
export const transformElement: NodeTransform = (node, context) => {
// perform the work on exit, after all child expressions have been
// processed and merged.
// 返回退出函数,在所有子表达式处理合并后执行
return function postTransformElement() {
node = context.currentNode!
if (
!(
node.type === NodeTypes.ELEMENT &&
(node.tagType === ElementTypes.ELEMENT ||
node.tagType === ElementTypes.COMPONENT)
)
) {
return
}
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
// The goal of the transform is to create a codegenNode implementing the
// VNodeCall interface.
// 转化的目标是创建一个实现VNodeCall接口的代码生成节点
let vnodeTag = isComponent
? resolveComponentType(node as ComponentNode, context)
: `"${tag}"`
const isDynamicComponent =
isObject(vnodeTag) && vnodeTag.callee === RESOLVE_DYNAMIC_COMPONENT
// 属性
let vnodeProps: VNodeCall['props']
// 子节点
let vnodeChildren: VNodeCall['children']
// 标记更新的类型标识,用于运行时优化
let vnodePatchFlag: VNodeCall['patchFlag']
let patchFlag: number = 0
// 动态绑定的属性
let vnodeDynamicProps: VNodeCall['dynamicProps']
let dynamicPropNames: string[] | undefined
let vnodeDirectives: VNodeCall['directives']
// 1.动态组件、svg、foreignObject 标签以及动态绑定key prop 的节点都被视作一个Block
// 为了运行时的更新优化,Vue.js 3.0 设计了一个 Block tree 的概念。Block tree 是一个将模版基于动态节点指令切割的嵌套
// 区块,每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体
// 大小相关提升为与动态内容的数量相关,极大优化了 diff 的效率,模板的动静比越大,这个优化就会越明显。
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'))
// props
// 2.处理 props:主要是从AST节点的props对象中进一步解析出指令vnodeDirectives、动态属性dynamicPropNames,以及更新标识
// patchFlag。这个标识只要用于标识节点更新的类型,在组件更新优化中会用到。
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
// 3.处理children:对于一个组件节点而言,如果有子节点,则说明是组件的插槽,另外还会有对一些内置组件KeepAlive、Teleport
// 的处理。
if (node.children.length > 0) {
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.
// 把KeepAlive看做是一个Block,这样可以避免他的子节点被父Block收集
shouldUseBlock = true
// 2. Force keep-alive to always be updated, since it uses raw children.
// 确保它始终更新
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 is not a real component and has dedicated runtime handling
// 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
if (
hasDynamicTextChild &&
getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
patchFlag |= PatchFlags.TEXT
}
// pass directly if the only child is a text node
// (plain / interpolation / expression)
// 如果只是一个普通文本节点、插值或表达式,直接把节点赋值给vnodeChildren
if (hasDynamicTextChild || type === NodeTypes.TEXT) {
vnodeChildren = child as TemplateTextChildNode
} else {
vnodeChildren = node.children
}
} else {
vnodeChildren = node.children
}
}
// patchFlag & dynamicPropNames
// 处理patchFlag 和 dynamicPropNames
if (patchFlag !== 0) {
if (__DEV__) {
if (patchFlag < 0) {
// special flags (negative and mutually exclusive)
vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
} else {
// bitwise flags
// 因为patchFlag本身就是一个数组, 获取 flag对应的名字,生成注释,方便理解生成代码对应节点的patchFlag。
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) {
// 把数组dynamicPropNames转化生成vnodeDynamicProps,便于后续对节点生成代码逻辑的处理
vnodeDynamicProps = stringifyDynamicPropNames(dynamicPropNames)
}
}
// 4.最后通过createVNodeCall创建实现了VNodeCall接口的代码生成节点
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
!!shouldUseBlock,
false /* disableTracking */,
isComponent,
node.loc
)
}
}
接着看createVNodeCall的实现:
// packages/compiler-core/src/ast.ts
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)
}
}
// 返回了一个对象,包含了传入的参数。要注意 context.helper 函数的调用,它会把一些 Symbol 对象添加到 context.helpers
// 数组中,目的是为了后续代码生成阶段,生成一些辅助代码。
return {
type: NodeTypes.VNODE_CALL,
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking,
isComponent,
loc
}
}
2.表达式节点转换:
// packages/compiler-core/src/transforms/transformExpression.ts
// 因为表达式不存在子节点,所以不需要退出函数,直接进入函数时做转换处理
export const transformExpression: NodeTransform = (node, context) => {
if (node.type === NodeTypes.INTERPOLATION) {
// 处理插值中的动态表达式
node.content = processExpression(
node.content as SimpleExpressionNode,
context
)
} else if (node.type === NodeTypes.ELEMENT) {
// handle directives on element
// 处理元素指令中的动态表达式
for (let i = 0; i < node.props.length; i++) {
const dir = node.props[i]
// do not process for v-on & v-for since they are special handled
// v-on 和 v-for不处理,因为他们都有各自的处理逻辑
if (dir.type === NodeTypes.DIRECTIVE && dir.name !== 'for') {
const exp = dir.exp
const arg = dir.arg
// do not process exp if this is v-on:arg - we need special handling
// for wrapping inline statements.
if (
exp &&
exp.type === NodeTypes.SIMPLE_EXPRESSION &&
!(dir.name === 'on' && arg)
) {
dir.exp = processExpression(
exp,
context,
// slot args must be processed as function params
dir.name === 'slot'
)
}
if (arg && arg.type === NodeTypes.SIMPLE_EXPRESSION && !arg.isStatic) {
dir.arg = processExpression(arg, context)
}
}
}
}
}
3.text节点转换:
// packages/compiler-core/src/transforms/transformText.ts
// Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
// perform the transform on node exit so that all expressions have already
// been processed.
// 在节点退出时执行转换,保证所有表达式都已经被处理
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
// 将相邻的文本节点合并
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child)) {
hasText = true
for (let j = i + 1; j < children.length; j++) {
const next = children[j]
if (isText(next)) {
if (!currentContainer) {
// 创建复合表达式节点
currentContainer = children[i] = createCompoundExpression(
[child],
child.loc
)
}
// merge adjacent text node into current
currentContainer.children.push(` + `, next)
children.splice(j, 1)
j--
} else {
currentContainer = undefined
break
}
}
}
}
if (
!hasText ||
// if this is a plain element with a single text child, leave it
// as-is since the runtime has dedicated fast path for this by directly
// setting textContent of the element.
// for component root it's always normalized anyway.
// 如果是一个带有单个文本子元素的纯元素节点,什么都不需要转换,因为这种情况在运行时可以直接设置元素的 textContent来更新文本。
(children.length === 1 &&
(node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
// #3756
// custom directives can potentially add DOM elements arbitrarily,
// we need to avoid setting textContent of the element at runtime
// to avoid accidentally overwriting the DOM elements added
// by the user through custom directives.
!node.props.find(
p =>
p.type === NodeTypes.DIRECTIVE &&
!context.directiveTransforms[p.name]
) &&
// in compat mode, <template> tags with no special directives
// will be rendered as a fragment so its children must be
// converted into vnodes.
!(__COMPAT__ && node.tag === 'template'))))
) {
return
}
// pre-convert text nodes into createTextVNode(text) calls to avoid
// runtime normalization.
// 为子文本节点创建一个调用函数表达式的代码生成节点
for (let i = 0; i < children.length; i++) {
const child = children[i]
if (isText(child) || child.type === NodeTypes.COMPOUND_EXPRESSION) {
const callArgs: CallExpression['arguments'] = []
// createTextVNode defaults to single whitespace, so if it is a
// single space the code could be an empty call to save bytes.
// 为createTextVNode添加执行参数
if (child.type !== NodeTypes.TEXT || child.content !== ' ') {
callArgs.push(child)
}
// mark dynamic text with flag so it gets patched inside a block
// 标记动态文本
if (
!context.ssr &&
getConstantType(child, context) === ConstantTypes.NOT_CONSTANT
) {
callArgs.push(
PatchFlags.TEXT +
(__DEV__ ? ` /* ${PatchFlagNames[PatchFlags.TEXT]} */` : ``)
)
}
children[i] = {
type: NodeTypes.TEXT_CALL,
content: child,
loc: child.loc,
codegenNode: createCallExpression(
context.helper(CREATE_TEXT),
callArgs
)
}
}
}
}
}
}
4.v-if节点转换:
// packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
// 退出回调函数,当所有子节点转换完成后执行
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) {
// v-if节点的退出函数
// 创建 IF 节点的codegenNode
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
// v-else-if、v-else节点的退出函数
// 将此分支的codegenNode附加到上一个条件的codegenNode 的alternate中
const parentCondition = getParentCondition(ifNode.codegenNode!)
// 更新候选节点
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
可以看到transformIf是通过createStructuralDirectiveTransform创建的一个结构化指令的转换函数,在vue.js中,v-if、v-else-if、v-else和v-for都属于结构化指令,因为它们能影响代码的组织结构。来看createStructuralDirectiveTransform的实现:
// packages/compiler-core/src/transform.ts
export function createStructuralDirectiveTransform(
// 指令的名称
name: string | RegExp,
// 构造转换退出函数的方法
fn: StructuralDirectiveTransform
): NodeTransform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
// 只处理元素节点,因为只有元素节点才会有v-if指令
if (node.type === NodeTypes.ELEMENT) {
const { props } = node
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
// 结构化指令的转换与插槽无关,插槽相关处理逻辑在 vSlot.ts中
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
return
}
const exitFns = []
// 解析这个节点的props属性,如果发现props包含v-if指令,那么从props删除这个结构化指令,防止无限递归
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
// structural directives are removed to avoid infinite recursion
// also we remove them *before* applying so that it can further
// traverse itself in case it moves the node around
// 删除结构指令以避免无限递归
props.splice(i, 1)
i--
const onExit = fn(node, prop, context)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}
最后执行的退出函数就是前面定义的匿名,执行了processIf函数,看一下实现:
// packages/compiler-core/src/transforms/vIf.ts
// target-agnostic transform used for both Client and SSR
export function processIf(
node: ElementNode,
dir: DirectiveNode,
context: TransformContext,
processCodegen?: (
node: IfNode,
branch: IfBranchNode,
isRoot: boolean
) => (() => void) | undefined
) {
if (dir.name === 'if') {
// 创建分支节点
const branch = createIfBranch(node, dir)
// 创建IF节点,替换当前节点
const ifNode: IfNode = {
type: NodeTypes.IF,
loc: node.loc,
branches: [branch]
}
context.replaceNode(ifNode)
if (processCodegen) {
return processCodegen(ifNode, branch, true)
}
} else {
// locate the adjacent v-if
// 处理 v-if 相邻节点,比如 v-else-if和v-else
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (
sibling &&
sibling.type === NodeTypes.TEXT &&
!sibling.content.trim().length
) {
context.removeNode(sibling)
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// Check if v-else was followed by v-else-if
if (
dir.name === 'else-if' &&
sibling.branches[sibling.branches.length - 1].condition === undefined
) {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
)
}
// move the node to the if node's branches
// 把节点移动到 IF 节点的 branches 中
context.removeNode()
const branch = createIfBranch(node, dir)
sibling.branches.push(branch)
const onExit = processCodegen && processCodegen(sibling, branch, false)
// since the branch was removed, it will not be traversed.
// make sure to traverse here.
// 因为分支已经被删除,所以他的子节点需要在这里遍历
traverseNode(branch, context)
// call on exit
// 执行退出函数
if (onExit) onExit()
// make sure to reset currentNode after traversal to indicate this
// node has been removed.
// 恢复 currentNode为null ,因为它已经被移除了
context.currentNode = null
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc)
)
}
break
}
}
}
// v-if处理逻辑,创建一个分支节点,这个分支节点是因为v-if节点内部的子节点可以属于一个分支,v-else-if和v-else节点内部的子节点
// 也是一个分支,最终页面显示取决于哪个分支的condition为true。所以分支节点返回的对象包含了condition和children
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
const isTemplateIf = node.tagType === ElementTypes.TEMPLATE
return {
type: NodeTypes.IF_BRANCH,
loc: node.loc,
condition: dir.name === 'else' ? undefined : dir.exp,
children: isTemplateIf && !findDir(node, 'for') ? node.children : [node],
userKey: findProp(node, `key`),
isTemplateIf
}
}
当 v-if 节点执行退出函数时,会通过 createCodegenNodeForBranch 创建 IF 分支节点的 codegenNode,我们来看一下它的实现:
// packages/compiler-core/src/transforms/vIf.ts
function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
createChildrenCodegenNode(branch, keyIndex, context),
// make sure to pass in asBlock: true so that the comment node call
// closes the current block.
createCallExpression(context.helper(CREATE_COMMENT), [
__DEV__ ? '"v-if"' : '""',
'true'
])
) as IfConditionalExpression
} else {
return createChildrenCodegenNode(branch, keyIndex, context)
}
}
// 当分支节点存在condition时,比如v-if和v-else-if,会通过createConditionalExpression返回一个条件表达式节点:
// packages/compiler-core/src/ast.ts
export function createConditionalExpression(
test: ConditionalExpression['test'],
consequent: ConditionalExpression['consequent'],
alternate: ConditionalExpression['alternate'],
newline = true
): ConditionalExpression {
return {
type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
test,
// IF主branch的子节点对应的代码生成节点
consequent,
// 后补branch子节点对应的代码生成节点
alternate,
newline,
loc: locStub
}
}
// 判断每个分支子节点是不是一个vnodeCall,如果这个子节点不是组件节点的话,把它转成一个BlockCall,也就是让v-if每一个分支都
// 可以创建一个Block
function createChildrenCodegenNode(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): BlockCodegenNode | MemoExpression {
const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
`${keyIndex}`,
false,
locStub,
ConstantTypes.CAN_HOIST
)
)
const { children } = branch
const firstChild = children[0]
const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT
if (needFragmentWrapper) {
if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
// optimize away nested fragments when child is a ForNode
const vnodeCall = firstChild.codegenNode!
injectProp(vnodeCall, keyProperty, context)
return vnodeCall
} else {
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
!branch.isTemplateIf &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
return createVNodeCall(
context,
helper(FRAGMENT),
createObjectExpression([keyProperty]),
children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
false,
false /* isComponent */,
branch.loc
)
}
} else {
const ret = (firstChild as ElementNode).codegenNode as
| BlockCodegenNode
| MemoExpression
const vnodeCall = getMemoedVNodeCall(ret)
// Change createVNode to createBlock.
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
makeBlock(vnodeCall, context)
}
// inject branch key
injectProp(vnodeCall, keyProperty, context)
return ret
}
}
开头知道AST转化有四部分:创建transform上下文、遍历AST节点、静态提升、创建根代码生成节点,现在接续看静态提升:
静态节点不依赖动态数据,一旦创建了就不会改变,所以只有静态节点才能被提升到外部创建。
静态提升是vue.js3.0编译阶段的优化策略,要在编译配置中查看是否配置了hoistStatic,回到transform函数,可以看到以下代码:
if (options.hoistStatic) {
hoistStatic(root, context)
}
看hoistStatic的实现:
// packages/compiler-core/src/transforms/hoistStatic.ts
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root,
context,
// Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes.
isSingleElementRoot(root, root.children[0])
)
}
export function isSingleElementRoot(
root: RootNode,
child: TemplateChildNode
): child is PlainElementNode | ComponentNode | TemplateNode {
const { children } = root
return (
children.length === 1 &&
child.type === NodeTypes.ELEMENT &&
!isSlotOutlet(child)
)
}
function walk(
node: ParentNode,
context: TransformContext,
doNotHoistNode: boolean = false
) {
const { children } = node
const originalCount = children.length
let hoistedCount = 0
for (let i = 0; i < children.length; i++) {
const child = children[i]
// only plain elements & text calls are eligible for hoisting.
// 只有普通元素和文本节点才能被静态提升
if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
// 获取静态节点的类型,如果是元素,则递归检查它的子节点
// 如果 getConstantType 返回的 staticType 的值是 2,则表明它是一个运行时常量,由于它的值在运行时才能被确定,所以是不能静态提升的。
: getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) {
// 更新 patchFlag
;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
// 更新节点的 codegenNode
child.codegenNode = context.hoist(child.codegenNode!)
hoistedCount++
continue
}
} else {
// node may contain dynamic children, but its props may be eligible for
// hoisting.
// 节点可能会包含一些动态子节点,但它的静态属性还是可以被静态提升
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.VNODE_CALL) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST
) {
const props = getNodeProps(child)
if (props) {
codegenNode.props = context.hoist(props)
}
}
if (codegenNode.dynamicProps) {
codegenNode.dynamicProps = context.hoist(codegenNode.dynamicProps)
}
}
}
} else if (
child.type === NodeTypes.TEXT_CALL &&
getConstantType(child.content, context) >= ConstantTypes.CAN_HOIST
) {
// 文本节点也可以静态提升
child.codegenNode = context.hoist(child.codegenNode)
hoistedCount++
}
// walk further
if (child.type === NodeTypes.ELEMENT) {
const isComponent = child.tagType === ElementTypes.COMPONENT
if (isComponent) {
context.scopes.vSlot++
}
// 递归遍历子节点
walk(child, context)
if (isComponent) {
context.scopes.vSlot--
}
} else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block
walk(child, context, child.children.length === 1)
} else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) {
// Do not hoist v-if single child because it has to be a block
walk(
child.branches[i],
context,
child.branches[i].children.length === 1
)
}
}
}
if (hoistedCount && context.transformHoist) {
// 如果编译配置了 transformHoist,则执行
context.transformHoist(children, context, node)
}
// all children were hoisted - the entire children array is hoistable.
if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT &&
node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children)
) {
node.codegenNode.children = context.hoist(
createArrayExpression(node.codegenNode.children)
)
}
}
// packages/compiler-core/src/transform.ts
// 创建transform上下文函数中的内置方法
hoist(exp) {
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
false,
exp.loc,
ConstantTypes.CAN_HOIST
)
identifier.hoisted = exp
return identifier
},
最后AST转换最后一步,创建根节点的代码生成节点:
// packages/compiler-core/src/transform.ts
// 为root这个虚拟的AST根节点创建一个代码生成节点
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
// if the single child is an element, turn it into a block.
// 如果子节点是单个元素节点,则将其转换成一个block
if (isSingleElementRoot(root, child) && child.codegenNode) {
// single element root is never hoisted so codegenNode will never be
// SimpleExpressionNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
makeBlock(codegenNode, context)
}
// 把这个child的codegenNode赋值给root的codegenNode
root.codegenNode = codegenNode
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.
// root codegen falls through via genNode()
root.codegenNode = child
}
} else if (children.length > 1) {
// root has multiple nodes - return a fragment block.
// 如果子节点多个节点,则返回一个fragment的代码生成节点
let patchFlag = PatchFlags.STABLE_FRAGMENT
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// check if the fragment actually contains a single valid child with
// the rest being comments
if (
__DEV__ &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
// 赋值给root的codegenNode
root.codegenNode = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
root.children,
patchFlag + (__DEV__ ? ` /* ${patchFlagText} */` : ``),
undefined,
undefined,
true,
undefined,
false /* isComponent */
)
} else {
// no children = noop. codegen will return null.
}
}