在 Vue.js 开发中,你是否曾遇到过这样的错误提示:
You are using the runtime-only build of Vue where the template compiler is not available.
或者在构建工具配置中看到 vue.runtime.esm-bundler.js 这样的文件名?这些提示都指向 Vue 框架中一个核心但常被误解 的概念------运行时(Runtime)与编译时(Compile-time)的区别。
本文将通过 Vue 3 的实际机制,彻底解析这两个概念,带你走完从 <template> 到真实 DOM 的完整旅程,帮你避免常见陷阱,提升对框架底层的理解。
一、什么是「编译时」和「运行时」?
1. 编译时(Compile-time)
- 定义 :代码在构建/打包阶段被处理的过程
- 发生位置:开发者的本地机器(通过 Vite、Webpack 等构建工具)
- 典型操作 :
- 解析
.vue文件中的<template> - 将模板编译成 JavaScript 的
render函数 - 静态节点提升、缓存优化
- Tree-shaking(移除未使用的代码)
- 生成最终可执行的 JS 包
- 解析
- 关键特征 :不发生在浏览器中,用户不可见
2. 运行时(Runtime)
- 定义 :代码在浏览器中实际执行的阶段
- 发生位置:用户的浏览器环境
- 典型操作 :
- 执行
render函数生成虚拟 DOM(VNode) - 响应式系统收集依赖、触发更新
- 虚拟 DOM diff 算法计算新旧树差异
- 批量更新真实 DOM
- 执行
- 依赖内容 :Vue 的运行时核心库(如
reactivity、renderer等模块) - 关键特征 :直接影响用户体验和性能
💡 通俗类比:
- 编译时 = 工厂把原材料加工成预制菜(模板 → render 函数)
- 运行时 = 用户在家加热预制菜并食用(render → 真实 DOM)
二、Vue 3 的两种构建版本:为什么有"运行时-only"?
Vue 3 提供了两种主要的构建产物,其核心区别在于是否包含模板编译器:
| 版本 | 是否含编译器 | 体积(gzip) | 使用场景 | 典型文件名 |
|---|---|---|---|---|
| 完整版(Full Build) | ✅ 是 | ~30KB | 直接在浏览器写模板(如 CDN 引入) | vue.global.js |
| 运行时-only(Runtime-only) | ❌ 否 | ~10KB | 现代工程化项目(Vite / Vue CLI) | vue.runtime.esm-bundler.js |
✅ 现代 Vue 项目默认使用运行时-only 版本,因为模板已在构建阶段被预编译。
为什么这样做?
- 体积更小:省去约 20KB 的编译器代码
- 性能更高:避免浏览器实时解析模板的开销
- 安全性更好 :杜绝运行时动态编译带来的 XSS 风险(如
v-html+ 动态模板)
三、从 <template> 到真实 DOM:完整流程拆解
我们以一个简单组件为例,看 Vue 如何完成整个生命周期:
vue
<!-- Counter.vue -->
<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
data() {
return {
title: '计数器',
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
第一步:编译时(构建阶段)
- Vite / Webpack 读取
.vue文件 @vue/compiler-sfc解析<template>- 模板编译器生成
render函数(简化示意):
js
// 编译后生成的 render 函数(实际更复杂)
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", { class: "counter" }, [
_createVNode("h2", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_createVNode("p", null, "Count: " + _toDisplayString(_ctx.count), 1 /* TEXT */),
_createVNode("button", { onClick: _ctx.increment }, "+1", 8 /* PROPS */, ["onClick"])
]))
}
🔍 注意:
_createVNode就是h()函数的内部实现,用于创建虚拟节点(VNode)
- 最终打包产物中不再包含原始模板字符串
第二步:运行时(浏览器执行)
- 浏览器加载
vue.runtime.esm-bundler.js - Vue 创建组件实例,挂载
render函数 - 首次渲染 :
- 调用
render()→ 返回 VNode 树 - 虚拟 DOM 渲染器(renderer)将 VNode 转为真实 DOM
- 插入到页面指定容器(如
#app)
- 调用
- 响应式更新 (当
count++时):- 触发
count的 setter - 通知依赖(即该组件的
render函数) - 重新执行
render生成新 VNode - Diff 新旧 VNode,仅更新
<p>文本内容 - 高效更新真实 DOM
- 触发
🌟 关键结论 :
浏览器中从未见过你的<template>!它早已在构建时变成了高效的 JavaScript 函数。
四、手写 render 函数:绕过编译时
如果你不需要模板,可以直接在运行时编写 render 函数:
js
import { createApp, h } from 'vue'
createApp({
data() {
return { msg: 'Hello Render!' }
},
render() {
return h('div', { style: { color: 'blue' } }, this.msg)
}
}).mount('#app')
适用场景:
- 动态生成 UI(如可视化编辑器)
- 高性能组件(避免模板解析开销)
- 与 JSX 混合使用(Vue 3 支持 JSX)
⚠️ 注意:即使手写
render,仍需依赖 Vue 的运行时核心(响应式、渲染器等)
五、常见误区与最佳实践
❌ 误区1:认为"运行时-only 不能用模板"
真相 :只要使用 .vue 单文件组件,构建工具会自动编译模板,完全兼容运行时-only
❌ 误区2:在运行时动态拼接模板字符串
js
// 危险且无效(运行时-only 下会报错)
createApp({
template: `<div>${dynamicContent}</div>` // ❌
})
正确做法 :用 render 函数或 v-html(注意 XSS 防护)
✅ 最佳实践:
- 工程化项目一律使用运行时-only
- 模板用于 90% 的常规组件
- 复杂动态逻辑用
render函数或 JSX - 不要手动引入完整版 Vue(除非特殊需求)
六、总结:一张图看懂全流程
[开发者写的 .vue 文件]
↓ (编译时:Vite/Webpack + vue/compiler-sfc)
[生成 render 函数 + JS 模块]
↓ (打包成 bundle.js)
[浏览器加载 vue.runtime + bundle.js]
↓ (运行时:Vue 响应式 + 渲染器)
[执行 render → VNode → 真实 DOM]
🔑 核心思想 :
Vue 3 将"编译"与"运行"彻底分离,实现"构建时优化,运行时轻量"这正是其性能优于许多竞品的关键设计。
结语
理解「运行时」与「编译时」,不仅是解决报错的关键,更是深入掌握 Vue 框架设计哲学的入口。当你下次再看到 runtime-only 字样时,你会明白:这不仅是一个技术选项,更是 Vue 对性能、安全与工程化的坚定选择。
记住 :
你写的不是模板,而是未来被执行的函数。
你部署的不是 HTML,而是经过精心编译的高效指令。