转换(Transform)阶段的工作
转换(Transform)阶段的工作
Vue3的编译过程分为三个主要阶段:解析(Parse)、转换(Transform)和生成(Codegen)。转换阶段位于解析之后,生成之前,负责对解析得到的AST进行各种处理和优化。这个阶段是Vue模板编译过程中最复杂的部分,包含了众多重要的功能实现。
AST节点转换
转换阶段首先会对解析阶段生成的原始AST进行节点转换。Vue内部通过transform
函数处理AST,这个函数会递归遍历AST树,对每个节点应用相应的转换函数:
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)
}
}
每个节点转换函数都会接收当前节点和转换上下文作为参数。例如,对于元素节点的转换:
const transformElement = (node, context) => {
// 处理props
const props = node.props
const vnodeTag = `"${node.tag}"`
const vnodeProps = buildProps(node, context)
// 处理子节点
const children = node.children
let vnodeChildren
if (children.length) {
vnodeChildren = children[0]
}
// 创建代码生成节点
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren
)
}
静态提升(Hoist Static)
Vue3的一个重要优化是静态提升,即在编译阶段识别出静态内容并将其提升到渲染函数之外。这样可以避免在每次重新渲染时重新创建这些静态节点:
function hoistStatic(root: RootNode, context: TransformContext) {
walk(root, context,
(node, context) => {
if (node.type === NodeTypes.ELEMENT && isStaticNode(node)) {
node.static = true
context.hoists.push(node)
return true
}
}
)
}
例如,对于模板<div><span>static</span><span>{{ dynamic }}</span></div>
,第一个<span>
会被提升:
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "static")
function render(_ctx) {
return _createVNode("div", null, [
_hoisted_1,
_createVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
])
}
补丁标志(Patch Flags)
转换阶段还会为动态节点添加补丁标志,帮助运行时更高效地更新DOM。这些标志指示了节点哪些部分需要被比较和更新:
function markDynamicProps(node: ElementNode) {
const flagNames = []
if (hasDynamicKeys(node.props)) {
flagNames.push('PROPS')
}
if (node.children.some(isDynamic)) {
flagNames.push('CHILDREN')
}
if (flagNames.length) {
node.patchFlag = getPatchFlag(flagNames)
}
}
例如,一个只有class动态的元素会被标记为8
(PROPS
标志),表示只需要比较props:
_createVNode("div", { class: _normalizeClass(_ctx.className) }, null, 8 /* PROPS */, ["class"])
缓存事件处理函数
为了避免不必要的重新渲染,Vue3会对事件处理函数进行缓存。转换阶段会识别出事件处理程序并添加缓存标志:
function transformExpression(node: Node, context: TransformContext) {
if (node.type === NodeTypes.ATTRIBUTE && node.name === 'on') {
const exp = node.value
if (exp.content.startsWith('_cache') || exp.isHandlerKey) {
return
}
node.value = {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_cache[${context.cacheIndex++}] || (_cache[${context.cacheIndex++}] = ${exp.content})`,
isStatic: false,
isConstant: false
}
}
}
转换后的代码会像这样:
_createVNode("button", { onClick: _cache[0] || (_cache[0] = $event => _ctx.handleClick($event)) }, "click me")
块(Block)处理
Vue3引入了块的概念,将模板划分为动态和静态区域。转换阶段会识别这些块并做相应标记:
function createStructuralDirectiveTransform(
name: string | RegExp,
fn: StructuralDirectiveTransform
): NodeTransform {
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const { props } = node
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && isMatch(name, prop.name)) {
fn(node, prop, context)
}
}
}
}
}
例如,v-if
指令会被转换为块:
// 模板: <div v-if="show">content</div>
function render(_ctx) {
return (_ctx.show)
? (_openBlock(), _createBlock("div", { key: 0 }, "content"))
: _createCommentVNode("v-if", true)
}
自定义指令转换
自定义指令在转换阶段也会被处理,转换为对应的运行时指令对象:
function transformDirectives(node: ElementNode, context: TransformContext) {
const directives = []
for (const prop of node.props) {
if (prop.type === NodeTypes.DIRECTIVE) {
directives.push({
name: prop.name,
rawName: prop.rawName,
value: prop.exp,
arg: prop.arg,
modifiers: prop.modifiers
})
}
}
if (directives.length) {
node.directives = directives
}
}
转换后的指令会合并到VNode的props中:
_createVNode("div", {
__directives: {
myDirective: {
mounted(el, binding) { /* ... */ }
}
}
})
插槽转换
插槽内容在转换阶段会被特殊处理,转换为插槽函数:
function transformSlotOutlet(node, context) {
if (node.type === NodeTypes.ELEMENT && node.tag === 'slot') {
const slotName = node.props.find(p => p.name === 'name')?.value || '"default"'
const slotProps = node.props.filter(p => p.name !== 'name')
node.codegenNode = createSlotOutlet(
slotName,
slotProps.length ? createObjectExpression(slotProps) : undefined
)
}
}
例如,一个具名插槽会被转换为:
_renderSlot(_ctx.$slots, "header", { item: _ctx.item })
静态类型推导
转换阶段还会进行一些静态分析,推导出表达式的类型信息,这些信息会在代码生成阶段被使用:
function inferExpressionType(
node: SimpleExpressionNode,
context: TransformContext
) {
if (node.isStatic) {
node.type = 'Literal'
} else if (context.identifiers.has(node.content)) {
node.type = 'Identifier'
} else if (node.content.startsWith('_ctx.')) {
node.type = 'MemberExpression'
}
}
这些类型信息可以帮助生成更优化的运行时代码,例如知道某个表达式是简单的属性访问,就可以生成直接的属性访问代码而不是全功能的解析代码。
源码结构分析
转换阶段的主要逻辑位于@vue/compiler-core
包的src/transform.ts
文件中。核心转换器由多个子转换器组成,每个负责处理特定的转换任务:
transform
├── transformElement // 处理元素节点
├── transformText // 处理文本节点
├── transformExpression // 处理表达式
├── transformIf // 处理v-if
├── transformFor // 处理v-for
├── transformOn // 处理事件
├── transformBind // 处理v-bind
├── transformModel // 处理v-model
└── ... // 其他转换
这种模块化的设计使得每个转换功能可以独立开发和测试,同时也方便按需组合使用。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:AST节点的数据结构
下一篇:代码生成(Codegen)的过程