模板预编译原理

Vue 模板预编译的过程和作用

问题

template 预编译是什么?它是如何工作的?

解答

模板预编译是指在构建阶段将模板字符串编译成渲染函数,而不是在浏览器运行时编译。

为什么需要预编译

// 运行时编译:浏览器中执行
// 需要包含编译器,体积大,性能差
new Vue({
  template: '<div>{{ message }}</div>'
})

// 预编译:构建时已完成编译
// 不需要编译器,体积小,性能好
new Vue({
  render(h) {
    return h('div', this.message)
  }
})

编译过程

模板编译分为三个阶段:

// 1. 解析(Parse)- 模板字符串 -> AST
const template = '<div id="app">{{ message }}</div>'

const ast = {
  tag: 'div',
  attrs: [{ name: 'id', value: 'app' }],
  children: [
    { type: 2, expression: '_s(message)', text: '{{ message }}' }
  ]
}

// 2. 优化(Optimize)- 标记静态节点
// 静态节点在 diff 时可以跳过
ast.static = false
ast.children[0].static = false

// 3. 生成(Generate)- AST -> 渲染函数代码
const code = `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}`

简化版编译器实现

// 简化的模板编译器
function compile(template) {
  // 1. 解析:提取标签和内容
  const ast = parse(template)
  
  // 2. 生成:转换为渲染函数
  const code = generate(ast)
  
  return new Function('h', code)
}

// 解析函数
function parse(template) {
  const tagMatch = template.match(/<(\w+)>(.+)<\/\1>/)
  if (tagMatch) {
    return {
      tag: tagMatch[1],
      children: parseChildren(tagMatch[2])
    }
  }
}

// 解析子节点
function parseChildren(content) {
  const children = []
  // 匹配插值表达式 {{ xxx }}
  const interpMatch = content.match(/\{\{\s*(\w+)\s*\}\}/)
  
  if (interpMatch) {
    children.push({
      type: 'interpolation',
      expression: interpMatch[1]
    })
  } else {
    children.push({
      type: 'text',
      content
    })
  }
  return children
}

// 生成渲染函数代码
function generate(ast) {
  const children = ast.children.map(child => {
    if (child.type === 'interpolation') {
      return `this.${child.expression}`
    }
    return `"${child.content}"`
  }).join(',')
  
  return `return h("${ast.tag}", null, ${children})`
}

// 使用
const render = compile('<div>{{ message }}</div>')
// 生成: function(h) { return h("div", null, this.message) }

预编译的实际应用

// vue-loader 在构建时将 .vue 文件中的 template 预编译

// 编译前:App.vue
/*
<template>
  <div class="app">
    <span>{{ count }}</span>
    <button @click="add">+1</button>
  </div>
</template>
*/

// 编译后:App.js
export default {
  render(_ctx) {
    return _createVNode("div", { class: "app" }, [
      _createVNode("span", null, _toDisplayString(_ctx.count)),
      _createVNode("button", { onClick: _ctx.add }, "+1")
    ])
  }
}

运行时 vs 预编译对比

特性运行时编译预编译
编译时机浏览器中构建时
包体积大(含编译器)
首屏性能
使用场景动态模板静态模板

关键点

  • 预编译在构建阶段完成,运行时无需编译器,减少包体积
  • 编译三阶段:解析(Parse)→ 优化(Optimize)→ 生成(Generate)
  • 解析阶段将模板字符串转换为 AST 抽象语法树
  • 优化阶段标记静态节点,diff 时可跳过
  • 生成阶段将 AST 转换为可执行的渲染函数