Vue.js 源码揭秘(五):编译器原理

16 阅读4分钟

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 编译器的核心:

  1. Parse:词法分析 + 语法分析,生成 AST
  2. Transform:遍历 AST,应用转换插件,生成 codegenNode
  3. Codegen:递归生成 render 函数代码
  4. 优化:静态提升、PatchFlags、Block Tree

📦 源码地址:github.com/vuejs/core

下一篇:Scheduler 调度器

如果觉得有帮助,欢迎点赞收藏 👍