Vue 模板编译原理

理解 Vue 模板编译的三个阶段:Parse、Optimize、Generate

问题

Vue 模板是如何编译成渲染函数的?解释 Parse、Optimize、Generate 三个阶段的作用。

解答

Vue 模板编译分为三个阶段:

模板字符串 → Parse → AST → Optimize → 优化后的AST → Generate → render函数

1. Parse(解析阶段)

将模板字符串解析成抽象语法树(AST)。

// 模板
const template = `<div id="app">{{ message }}</div>`

// 解析后的 AST 结构
const ast = {
  tag: 'div',
  type: 1, // 1: 元素节点
  attrs: [{ name: 'id', value: 'app' }],
  children: [
    {
      type: 2, // 2: 带表达式的文本节点
      expression: '_s(message)', // _s 是 toString 方法
      text: '{{ message }}'
    }
  ]
}

简化版解析器实现:

function parse(template) {
  // 匹配开始标签
  const startTagRE = /^<([a-zA-Z]+)([^>]*)>/
  // 匹配结束标签
  const endTagRE = /^<\/([a-zA-Z]+)>/
  // 匹配插值表达式
  const interpolationRE = /\{\{(.+?)\}\}/g

  let index = 0
  const root = { children: [] }
  const stack = [root]

  while (index < template.length) {
    const current = stack[stack.length - 1]
    const rest = template.slice(index)

    if (rest.startsWith('</')) {
      // 处理结束标签
      const match = rest.match(endTagRE)
      if (match) {
        stack.pop()
        index += match[0].length
      }
    } else if (rest.startsWith('<')) {
      // 处理开始标签
      const match = rest.match(startTagRE)
      if (match) {
        const node = {
          tag: match[1],
          type: 1,
          children: [],
          attrs: parseAttrs(match[2])
        }
        current.children.push(node)
        stack.push(node)
        index += match[0].length
      }
    } else {
      // 处理文本内容
      const endIndex = rest.indexOf('<')
      const text = endIndex === -1 ? rest : rest.slice(0, endIndex)
      if (text.trim()) {
        current.children.push({
          type: interpolationRE.test(text) ? 2 : 3,
          text: text.trim()
        })
      }
      index += text.length
    }
  }

  return root.children[0]
}

function parseAttrs(attrStr) {
  const attrs = []
  const attrRE = /([a-zA-Z-]+)="([^"]*)"/g
  let match
  while ((match = attrRE.exec(attrStr))) {
    attrs.push({ name: match[1], value: match[2] })
  }
  return attrs
}

2. Optimize(优化阶段)

标记静态节点和静态根节点,在后续更新时跳过这些节点的 diff 比较。

function optimize(node) {
  // 标记是否为静态节点
  markStatic(node)
  // 标记静态根节点
  markStaticRoot(node)
}

function markStatic(node) {
  node.static = isStatic(node)

  if (node.type === 1) {
    // 递归标记子节点
    for (const child of node.children) {
      markStatic(child)
      // 子节点非静态,父节点也非静态
      if (!child.static) {
        node.static = false
      }
    }
  }
}

function isStatic(node) {
  // 带表达式的文本节点不是静态的
  if (node.type === 2) return false
  // 纯文本节点是静态的
  if (node.type === 3) return true
  // 元素节点需要检查是否有动态绑定
  return !node.attrs?.some(attr =>
    attr.name.startsWith(':') ||
    attr.name.startsWith('@') ||
    attr.name.startsWith('v-')
  )
}

function markStaticRoot(node) {
  if (node.type === 1) {
    // 静态根节点:本身是静态的,且有子节点,且子节点不只是一个纯文本节点
    if (node.static && node.children.length &&
        !(node.children.length === 1 && node.children[0].type === 3)) {
      node.staticRoot = true
      return
    }
    node.staticRoot = false
    // 递归处理子节点
    for (const child of node.children) {
      markStaticRoot(child)
    }
  }
}

3. Generate(生成阶段)

将 AST 转换成 render 函数字符串。

function generate(node) {
  const code = genElement(node)
  return {
    render: `with(this) { return ${code} }`
  }
}

function genElement(node) {
  if (node.type === 1) {
    // 元素节点
    const tag = `'${node.tag}'`
    const data = genData(node)
    const children = genChildren(node)
    return `_c(${tag}${data ? `,${data}` : ''}${children ? `,${children}` : ''})`
  } else if (node.type === 2) {
    // 带表达式的文本
    return `_v(_s(${node.text.replace(/\{\{(.+?)\}\}/g, '$1').trim()}))`
  } else {
    // 纯文本
    return `_v('${node.text}')`
  }
}

function genData(node) {
  if (!node.attrs?.length) return ''
  const attrs = node.attrs.map(a => `${a.name}:"${a.value}"`).join(',')
  return `{attrs:{${attrs}}}`
}

function genChildren(node) {
  if (!node.children?.length) return ''
  return `[${node.children.map(genElement).join(',')}]`
}

完整示例

// 编译入口
function compile(template) {
  // 1. 解析模板生成 AST
  const ast = parse(template)

  // 2. 优化 AST
  optimize(ast)

  // 3. 生成 render 函数
  const code = generate(ast)

  return code
}

// 测试
const template = `<div id="app"><span>Hello</span><p>{{ name }}</p></div>`
const result = compile(template)
console.log(result.render)
// with(this) { return _c('div',{attrs:{id:"app"}},[_c('span',,[_v('Hello')]),_c('p',,[_v(_s(name))])]) }

render 函数中的辅助方法:

// Vue 内部的渲染辅助方法
_c = createElement  // 创建元素 VNode
_v = createTextVNode // 创建文本 VNode
_s = toString       // 转字符串

关键点

  • Parse:通过正则和状态机将模板解析成 AST,记录标签、属性、指令等信息
  • Optimize:标记静态节点,patch 时直接跳过,减少 diff 开销
  • Generate:递归遍历 AST,拼接成 render 函数代码字符串
  • 编译可以在构建时完成(vue-loader),也可以在运行时完成(完整版 Vue)
  • 静态节点只会渲染一次,后续更新时复用之前的 VNode