Vue.js 源码揭秘(五):编译器原理
本文深入 compiler-core 源码,解析模板编译的 parse、transform、codegen 三阶段。
一、编译流程
┌─────────────────────────────────────────────────────────────┐
│ 编译流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Template String │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ Parse │ ──► 词法分析 + 语法分析 ──► AST │
│ └─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ Transform │ ──► 遍历 AST,应用转换插件 ──► 优化后 AST │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ │
│ │ Codegen │ ──► 生成 render 函数代码 │
│ └─────────┘ │
│ │ │
│ ▼ │
│ Render Function │
│ │
└─────────────────────────────────────────────────────────────┘
二、compile 入口
// packages/compiler-core/src/compile.ts
export function baseCompile(
source: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 1. Parse:解析模板为 AST
const ast = isString(source) ? baseParse(source, options) : source
// 2. Transform:转换 AST
transform(
ast,
extend({}, options, {
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || [])
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {}
)
})
)
// 3. Codegen:生成代码
return generate(ast, extend({}, options, {
prefixIdentifiers
}))
}
三、Parse 阶段
3.1 AST 节点类型
// packages/compiler-core/src/ast.ts
export enum NodeTypes {
ROOT, // 根节点
ELEMENT, // 元素
TEXT, // 文本
COMMENT, // 注释
SIMPLE_EXPRESSION, // 简单表达式
INTERPOLATION, // 插值 {{ }}
ATTRIBUTE, // 属性
DIRECTIVE, // 指令
// 容器
COMPOUND_EXPRESSION, // 复合表达式
IF, // v-if
IF_BRANCH, // v-if 分支
FOR, // v-for
TEXT_CALL, // createTextVNode
// codegen
VNODE_CALL, // createVNode
JS_CALL_EXPRESSION, // 函数调用
JS_OBJECT_EXPRESSION, // 对象表达式
JS_PROPERTY, // 对象属性
JS_ARRAY_EXPRESSION, // 数组表达式
JS_FUNCTION_EXPRESSION, // 函数表达式
JS_CONDITIONAL_EXPRESSION, // 条件表达式
JS_CACHE_EXPRESSION // 缓存表达式
}
3.2 baseParse
// packages/compiler-core/src/parser.ts
export function baseParse(input: string, options: ParserOptions = {}): RootNode {
const context = createParserContext(input, options)
const start = getCursor(context)
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
function createParserContext(content: string, options: ParserOptions): ParserContext {
return {
options: extend({}, defaultParserOptions, options),
column: 1,
line: 1,
offset: 0,
originalSource: content,
source: content,
inPre: false,
inVPre: false
}
}
3.3 parseChildren
function parseChildren(
context: ParserContext,
mode: TextModes,
ancestors: ElementNode[]
): TemplateChildNode[] {
const nodes: TemplateChildNode[] = []
while (!isEnd(context, mode, ancestors)) {
const s = context.source
let node: TemplateChildNode | undefined
if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
if (startsWith(s, context.options.delimiters[0])) {
// 插值 {{ }}
node = parseInterpolation(context, mode)
} else if (mode === TextModes.DATA && s[0] === '<') {
if (s[1] === '!') {
if (startsWith(s, '<!--')) {
// 注释
node = parseComment(context)
} else if (startsWith(s, '<!DOCTYPE')) {
// DOCTYPE
node = parseBogusComment(context)
}
} else if (s[1] === '/') {
// 结束标签
parseTag(context, TagType.End, parent)
continue
} else if (/[a-z]/i.test(s[1])) {
// 元素
node = parseElement(context, ancestors)
}
}
}
if (!node) {
// 文本
node = parseText(context, mode)
}
if (isArray(node)) {
nodes.push(...node)
} else {
nodes.push(node)
}
}
return nodes
}
3.4 parseElement
function parseElement(
context: ParserContext,
ancestors: ElementNode[]
): ElementNode | undefined {
// 解析开始标签
const element = parseTag(context, TagType.Start, parent)
// 自闭合标签
if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
return element
}
// 递归解析子节点
ancestors.push(element)
const mode = context.options.getTextMode(element, parent)
const children = parseChildren(context, mode, ancestors)
ancestors.pop()
element.children = children
// 解析结束标签
if (startsWithEndTagOpen(context.source, element.tag)) {
parseTag(context, TagType.End, parent)
}
return element
}
3.5 parseTag
function parseTag(
context: ParserContext,
type: TagType,
parent: ElementNode | undefined
): ElementNode {
// 匹配标签名
const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
const tag = match[1]
advanceBy(context, match[0].length)
advanceSpaces(context)
// 解析属性
const props = parseAttributes(context, type)
// 自闭合
let isSelfClosing = false
if (context.source.length === 0) {
// error
} else {
isSelfClosing = startsWith(context.source, '/>')
advanceBy(context, isSelfClosing ? 2 : 1)
}
// 判断标签类型
let tagType = ElementTypes.ELEMENT
if (tag === 'slot') {
tagType = ElementTypes.SLOT
} else if (tag === 'template') {
if (props.some(p => p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name))) {
tagType = ElementTypes.TEMPLATE
}
} else if (isComponent(tag, props, context)) {
tagType = ElementTypes.COMPONENT
}
return {
type: NodeTypes.ELEMENT,
tag,
tagType,
props,
isSelfClosing,
children: [],
loc: getSelection(context, start)
}
}
四、Transform 阶段
4.1 transform 入口
// packages/compiler-core/src/transform.ts
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
// 遍历 AST
traverseNode(root, context)
// 静态提升
if (options.hoistStatic) {
hoistStatic(root, context)
}
// 创建根节点 codegenNode
if (!options.ssr) {
createRootCodegen(root, context)
}
// 收集元信息
root.helpers = new Set([...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
}
4.2 traverseNode
export function traverseNode(
node: RootNode | TemplateChildNode,
context: TransformContext
) {
context.currentNode = node
// 应用 nodeTransforms
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) {
// 节点被移除
return
}
node = context.currentNode
}
// 递归处理子节点
switch (node.type) {
case NodeTypes.COMMENT:
context.helper(CREATE_COMMENT)
break
case NodeTypes.INTERPOLATION:
context.helper(TO_DISPLAY_STRING)
break
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
}
// 执行退出函数(逆序)
context.currentNode = node
let i = exitFns.length
while (i--) {
exitFns[i]()
}
}
4.3 核心转换插件
// 内置 nodeTransforms
export const nodeTransforms = [
transformOnce, // v-once
transformIf, // v-if/v-else-if/v-else
transformMemo, // v-memo
transformFor, // v-for
transformExpression, // 表达式
transformSlotOutlet, // <slot>
transformElement, // 元素
trackSlotScopes, // 插槽作用域
transformText // 文本
]
// 内置 directiveTransforms
export const directiveTransforms = {
bind: transformBind, // v-bind
cloak: noopDirectiveTransform,
html: transformVHtml, // v-html
text: transformVText, // v-text
model: transformModel, // v-model
on: transformOn, // v-on
show: transformShow // v-show
}
4.4 transformElement
// packages/compiler-core/src/transforms/transformElement.ts
export const transformElement: NodeTransform = (node, context) => {
return function postTransformElement() {
const { tag, props } = node
// 分析 props
let vnodeProps
let vnodePatchFlag
let vnodeDynamicProps
let vnodeDirectives
// 处理 props
if (props.length > 0) {
const propsBuildResult = buildProps(node, context)
vnodeProps = propsBuildResult.props
vnodePatchFlag = propsBuildResult.patchFlag
vnodeDynamicProps = propsBuildResult.dynamicPropNames
vnodeDirectives = propsBuildResult.directives
}
// 处理 children
let vnodeChildren
if (node.children.length > 0) {
if (node.children.length === 1) {
const child = node.children[0]
if (child.type === NodeTypes.TEXT) {
vnodeChildren = child
} else {
vnodeChildren = node.children
}
} else {
vnodeChildren = node.children
}
}
// 创建 codegenNode
node.codegenNode = createVNodeCall(
context,
vnodeTag,
vnodeProps,
vnodeChildren,
vnodePatchFlag,
vnodeDynamicProps,
vnodeDirectives,
shouldUseBlock(node, vnodePatchFlag)
)
}
}
五、Codegen 阶段
5.1 generate 入口
// packages/compiler-core/src/codegen.ts
export function generate(
ast: RootNode,
options: CodegenOptions = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
const { mode, push, indent, deindent, newline } = context
// 生成前置代码
genFunctionPreamble(ast, context)
// 生成 render 函数
const functionName = 'render'
const args = ['_ctx', '_cache']
push(`function ${functionName}(${args.join(', ')}) {`)
indent()
// with 模式
if (!options.prefixIdentifiers) {
push(`with (_ctx) {`)
indent()
}
// 生成 helpers
if (ast.helpers.size > 0) {
push(`const { ${[...ast.helpers].map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`).join(', ')} } = _Vue`)
newline()
}
// 生成 return
push(`return `)
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (!options.prefixIdentifiers) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: '',
map: context.map ? context.map.toJSON() : undefined
}
}
5.2 genNode
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
}
}
5.3 genVNodeCall
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock,
disableTracking
} = node
if (directives) {
push(helper(WITH_DIRECTIVES) + `(`)
}
if (isBlock) {
push(`(${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ``}), `)
}
// createElementVNode / createBlock
const callHelper = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`)
// 生成参数
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
)
push(`)`)
if (isBlock) {
push(`)`)
}
if (directives) {
push(`, `)
genNode(directives, context)
push(`)`)
}
}
六、编译示例
输入模板
<div id="app">
<span>{{ msg }}</span>
<button @click="handleClick">Click</button>
</div>
输出代码
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", { id: "app" }, [
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
_createElementVNode("button", { onClick: _ctx.handleClick }, "Click", 8 /* PROPS */, ["onClick"])
]))
}
七、编译优化
7.1 静态提升
// 静态节点提升到 render 函数外
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "static text", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1)
]))
}
7.2 PatchFlags
// 标记动态内容类型
TEXT = 1 // 动态文本
CLASS = 2 // 动态 class
STYLE = 4 // 动态 style
PROPS = 8 // 动态 props
FULL_PROPS = 16 // 有动态 key
NEED_HYDRATION = 32
STABLE_FRAGMENT = 64
KEYED_FRAGMENT = 128
UNKEYED_FRAGMENT = 256
NEED_PATCH = 512
DYNAMIC_SLOTS = 1024
HOISTED = -1 // 静态提升
BAIL = -2 // 退出优化
八、小结
Vue3 编译器的核心:
- Parse:词法分析 + 语法分析,生成 AST
- Transform:遍历 AST,应用转换插件,生成 codegenNode
- Codegen:递归生成 render 函数代码
- 优化:静态提升、PatchFlags、Block Tree
📦 源码地址:github.com/vuejs/core
下一篇:Scheduler 调度器
如果觉得有帮助,欢迎点赞收藏 👍