Vue 中到底哪些代码是编译期执行的?你写的代码可能根本没运行

300 阅读5分钟

“你以为写了代码,它就一定会被运行?”
不,在 Vue 中,有些代码不仅不会被运行,甚至在运行期已经彻底被消除了。

Vue 从 Vue 3 开始,进入了“编译期优化”的时代。大量代码在运行前就被静态分析、优化、裁剪了。这对于性能是好事,但对开发者来说也提出了更高要求:你得知道哪些是编译期行为,否则你调试半天的代码,可能根本就没进入运行时。

本文我们会深入分析 Vue 编译期机制,包括具体的优化策略、代码实例对比、调试技巧和真实项目中容易踩的坑。


什么是编译期、运行期?

Vue 的架构可以简单理解为两个阶段:

  • 编译期(Compile-time) :template 被转换为 render 函数,进行静态提升、结构优化、patch 减少等。
  • 运行期(Runtime) :组件挂载后,响应式系统、生生命周期、更新 DOM 等。

例如:

<template>
  <div>{{ count + 1 }}</div>
</template>

Vue 会编译为:

function render(ctx, cache) {
  return _createElementVNode("div", null, _toDisplayString(ctx.count + 1))
}

但如果你用了 v-once、v-if="false" 等,甚至 count + 1 会被提前计算为固定值,直接变成:

_createElementVNode("div", null, "2")

你写的 count 根本没运行!


哪些是典型的编译期优化点?

1. v-if 搭配静态条件,直接被移除

<template>
  <div v-if="false">不会出现</div>
  <div v-else>我才会显示</div>
</template>

编译结果:

_createElementVNode("div", null, "我才会显示")

第一段 v-if 被移除了。

如果你写的是:

<div v-if="1 > 2">不会</div>

它也会被静态分析掉。

但如果你写的是 v-if="someValue",那 Vue 就无法在编译期确定,只能留到运行时判断。

2. v-once:只渲染一次的表达式,直接静态提升

<div v-once>{{ new Date().toLocaleString() }}</div>

你以为它会显示最新时间?不,它只显示“编译那一刻”的值。

编译结果:

_createStaticVNode("2025/06/25 10:23:12", 1)

这在一些“默认图标”、“初始文案”中非常常见。如果你不小心给动态表达式加上了 v-once,那调试永远不会变!

3. v-memo:手动缓存的强力优化器

<div v-memo="[id]">
  {{ computeHeavy(id) }}
</div>

只要 id 不变,computeHeavy(id) 根本不会再次执行。

Vue 在编译期生成缓存指令,跳过整个模板 block。

if (_memo[0] === id) {
  return _cachedVNode[0];
}

非常适合 expensive 的区域,例如图表组件、复杂嵌套 slot 的列表等。

4. 静态组件类型:v-is 被直接解构

<component v-is="'span'">静态组件</component>

直接编译为:

_createElementVNode("span", null, "静态组件")

和你手写 <span> 一样,Vue 不会保留动态组件的开销。

5. @vue:mounted 是 compiler 专用 hook,不是事件

<div @vue:mounted="handleMounted"></div>

这是一个“编译器钩子” ,不是普通事件绑定。

它在 vnode 被挂载完成后由编译器自动触发,仅适用于 SSR hydration 场景,不会自动绑定在 DOM 上。

你以为是 addEventListener("vue:mounted")?不,是编译器内部指令。


用代码看差异:写法与编译结果对照

我们以几个写法举例:

v-once 的时间显示

<div v-once>{{ Date.now() }}</div>

你以为编译后:

_createElementVNode("div", null, _toDisplayString(Date.now()))

实际是:

_createStaticVNode("1729902398123", 1)

彻底静态!没任何 runtime。

条件判断提前裁剪

<div>
  <p v-if="true">Hello</p>
  <p v-else>World</p>
</div>
_createElementVNode("div", null, [
  _createElementVNode("p", null, "Hello")
])

静态组件解构

<component v-is="'h1'">Hi</component>
_createElementVNode("h1", null, "Hi")

你以为用了 component 动态能力?不,Vue 判断出它是静态标签,直接解构。


编译期优化的坑

这些特性优化了性能,也可能让你掉坑:

v-once 放错位置,数据永远不更新

<template>
  <div>
    <h1 v-once>{{ title }}</h1>
    <button @click="title = 'New'">change</button>
  </div>
</template>

你点击按钮后 title 改了,但页面不更新。因为 v-once 导致它在编译时就固定了。

❌ 静态表达式导致逻辑无法触发

<component v-is="'div'" @click="handleClick"></component>

你在调试中怎么点都不触发 handler,原因是它被编译为:

<div></div>

没有事件绑定逻辑了!因为静态类型不会走 resolveDynamicComponent

❌ 误用 v-memo 导致组件不更新

<div v-memo="[]">{{ new Date() }}</div>

没参数,永远不会更新!等于只渲染一次。


你写的 Vue 代码,有很大一部分压根不是运行时逻辑

编译期优化的威力:

  • 性能更强(避免不必要的运算和更新)
  • 文件体积小(很多表达式都被移除)
  • 可读性高(render 函数更简洁)

但代价是:你必须了解哪些语法是编译期行为,否则很可能写了“看起来能运行,实际根本不会执行”的代码。

Vue 编译期行为速查表:

特性是否编译期有什么影响
v-if 搭配静态值✅ 是条件块被直接移除
v-once✅ 是内容静态提升,不再响应
v-memo✅ 是动态缓存,跳过渲染
v-is 静态✅ 是解构成原生标签
@vue:mounted✅ 是编译器插桩,不是 DOM 事件
v-bind 动态❌ 否保留运行时更新能力
v-for❌ 否保留运行时生成能力

参考阅读与源码

如果你打算进一步了解 Vue 是怎么判断哪些部分是静态、哪些要动态渲染,可以深入研究 Vue 的静态提升逻辑 hoistStatic()

Thank you 🙂

📌 你可以继续看我的系列文章