文章目录
前言
Vue 3 中的编译时 vs 运行时区别
模板在编译时转化为渲染函数
编译时的优化处理
运行时的工作:创建组件实例与渲染流程
前言
详细整理 Vue 3 中编译时和运行时的概念区别,并重点解释为什么组件实例是在运行时创建的。
我会结合官方文档、源码分析和社区解释,确保内容清晰易懂,并配有示意图来说明组件生命周期中的创建时机。
Vue 3 中的编译时 vs 运行时区别
在 Vue 3 中,编译时(compile time)指的是框架在运行应用之前,对组件模板进行的编译处理;而运行时(runtime)是指应用实际执行、组件实例创建和更新 DOM 的阶段。简而言之,编译时将模板“翻译”成渲染函数代码,运行时则执行这些代码来创建组件实例、处理响应式数据并操作真实 DOM。
在 Vue 3 中,模板会先被编译为渲染函数代码,然后在运行时执行渲染函数产出虚拟 DOM 树,并据此更新真实 DOM。组件实例持有响应式状态,当状态变化时会触发重新渲染和虚拟 DOM patch 更新真实 DOM。

模板在编译时转化为渲染函数
Vue 采用类似 Vue 2 的策略:提供类 HTML 的模板语法,但会在编译时将模板编译成等价的 渲染函数,该渲染函数返回虚拟 DOM (VNode) 树。这个编译过程主要分三步:
解析 (parse): 将模板字符串解析生成对应的 抽象语法树 (AST)。AST 是模板的结构化表示,包含节点类型、属性等信息,用于后续优化和代码生成。在这一步,编译器可以识别模板中的指令和结构,例如 v-if、v-for、自定义组件标签等。
转换 (transform): 对 AST 进行一系列转换和优化处理。Vue 3 引入了插件化的 AST 转换管线,每种指令 (如 v-if、v-for、v-model 等) 都对应一个转换插件,对 AST 节点进行操作。同时,编译器在此阶段对模板进行静态分析,找出模板中不会改变的静态内容和结构。对于静态节点或静态子树,编译器会做标记,并执行静态提升等优化:将这些静态节点提升为常量,从渲染函数中提取出来。这样在每次重新渲染时就无需重复创建和对比这些不变的节点,提高运行时性能。
代码生成 (generate): 将优化后的 AST 转换为对应的渲染函数代码字符串。最终得到的渲染函数会返回虚拟 DOM 树。例如,一个简单模板 <h1>Hello, { 编译后可能生成类似的渲染函数(简化示意):
{ name }}</h1>
function render(ctx) {
with(ctx) {
return _createElementVNode('h1', null, 'Hello, ' + name)
}
}
渲染函数通过作用域提升(如使用 with 将组件实例的 proxy 对象作为作用域)来直接访问 name 等响应式数据。经过这三步后,模板被“翻译”成高效的 JavaScript 渲染代码。
值得注意的是,Vue 3 的模板编译既可以在构建时提前完成(例如单文件组件 SFC 在构建工具下由 vue/compiler-sfc 编译),也可以在浏览器中 运行时按需编译(需引入支持运行时编译的 Vue 构建版本)。如果组件在运行时没有预编译的渲染函数,Vue 会在初始化组件时检查模板并调用编译器将其转为渲染函数。
编译时的优化处理
编译阶段除了生成渲染函数,还承担了大量优化工作,以减少后续运行时的开销:
静态提升与静态节点合并: 编译器会标记模板中完全静态的内容(没有依赖响应式数据或不会改变的部分)。这些静态节点在生成代码时被提升到渲染函数外部,只创建一次。运行时会缓存这些静态节点的 VNode,在后续更新中重复使用,而不必每次重新创建和比对它们。如果模板中有大量连续静态节点,编译器甚至会将它们合并成一个静态片段,直接通过 innerHTML 插入,从而减少虚拟节点的数量。
指令转换与提取: 在 transform 阶段,编译器根据每个指令的定义对 AST 做转换。例如,v-if 指令会被编译成三元表达式或条件块,v-for 会转换成迭代渲染的结构,v-model 则拆解为 prop 更新和 update: 事件。这样,模板中的高级指令语法被“提取”并转换成等价的低级渲染逻辑,在运行时无需再做额外解析。指令钩子(如自定义指令的 beforeMount, updated 等) 也会在编译阶段生成绑定代码,以便运行时直接调用。
Patch Flag 优化标记: Vue 3 的编译器会为模板中带有动态绑定的元素添加优化标记 (patch flags)。这些标记是一些位标志,告诉运行时哪个部分是动态的以及如何比对。例如,一个只有动态 class 的元素会被标记为仅需要比较 class:生成的代码中会有一个整型标志参数代表 “CLASS” 类型更新。运行时据此可以快速判断更新时需要执行的最小操作。多个标志会合并为一个数字,通过按位运算检查。借助这些优化标记,Vue 运行时在更新动态内容时能够走捷径,只处理必要变动而跳过无关部分。
Block tree & Tree Flattening: Vue 3 编译器将模板划分为不同的 块 (block),每个块内部的节点结构在运行时被认为是稳定的(除非有 v-if/v-for 这样改变结构的指令)。编译器使用 _openBlock() 和 _createElementBlock() 来生成这些块状的 vnode 树。每个块会跟踪其内部所有带动态绑定的后代节点列表,从而在更新时可以快速遍历这些需要 diff 的节点,而无需递归整个子树。这种 树扁平化 的处理进一步减少了运行时遍历开销,实现更高效的差分更新。
通过以上种种编译时优化,Vue 3 在确保模板灵活性的同时,把尽可能多的工作前移到编译阶段完成。编译器产出的渲染函数中包含了关于动态静态部分的提示信息,使得运行时可以用更高效的方式创建和更新 DOM。
运行时的工作:创建组件实例与渲染流程
在运行时,Vue 接管编译后的渲染函数,负责创建组件实例、初始化响应式系统,并执行渲染和后续更新等流程。大致可分为以下几个步骤:
应用挂载 & 根组件渲染: 当我们调用 createApp(App).mount('#app') 时,Vue 会创建应用实例并生成根组件的虚拟节点 (VNode)。随后调用内部渲染器的 render(vnode, container) 将其挂载到指定容器中。渲染器会递归地构建虚拟 DOM 树并将其挂载为真实 DOM,这个过程称为 mount (挂载)。初次挂载时,旧的 vnode 为空,因此渲染器会走创建流程。
组件实例的创建: 当渲染器在虚拟 DOM 树中遇到一个组件节点(即 vnode 的类型是一个组件定义,而非原生元素)时,Vue 在运行时创建该组件的实例对象【12†图】。这一过程由框架内部的 createComponentInstance 函数完成,它类似于构造函数,实例化了一个包含组件所有状态和配置的 组件实例。组件实例会记录组件的类型、父组件引用、应用上下文、props 数据、emit 事件等信息,并初始化一些属性容器(如 state、props、slots、setupState 等)。需要强调,组件实例的创建发生在运行时:编译阶段只有模板的代码和元信息,只有在实际渲染时才能根据传入的 props、父子关系等上下文创建出具体的实例对象。
创建实例后,Vue 会进行依赖注入和初始化:首先解析传入的 props 数据并注入到实例,设置初始的插槽内容等。如果使用的是 Options API,则在这时调用组件的 beforeCreate 钩子【12†图】。接下来调用内部的 setupComponent 函数进入组件设置阶段。
响应式系统启动: 在 setupComponent 阶段,Vue 会根据组件定义启用响应式系统,为组件的状态建立观测 (reactivity)。对于 Options API 组件,这包括将 data() 返回的对象转换为 reactive 对象或通过 Proxy 将其属性转为响应式,并把 methods、computed 等合并进实例;对于 Composition API 组件,则直接调用其 setup() 函数。值得一提的是,Vue 3 中组件实例的 this 并不是原始数据对象本身,而是一个被 Proxy 代理的对象。在渲染函数执行时,Vue 会将组件实例的 proxy 作为渲染上下文 (例如通过 with (proxy) { ... }),从而在模板中直接使用 this.property 或 property 来触发响应式读取。通过这种机制,组件的响应式状态正式激活:一旦有响应式数据变化,Vue 能够追踪到并触发后续的视图更新。
执行组件的初始化逻辑: 在实例创建并注入依赖后,如果组件采用 Composition API,则立即执行其 setup() 函数(在 Options API 的 beforeCreate 之后,created 之前)【12†图】。setup() 可以返回一个对象或函数:返回对象时,其属性会被合并为组件实例的本地状态;返回渲染函数则作为组件的渲染函数覆盖模板编译结果。无论哪种情况,Composition API 下我们通常使用 Vue 的响应式 API (ref, reactive 等) 定义状态,此时这些响应式状态也会与Vue的依赖追踪系统连接起来。
对于 Options API,Vue 会在这时完成 data 数据的观测转换,调用用户定义的 created 钩子函数,然后继续后续流程【12†图】。需要注意,在 Vue 3 中 Options API 的 beforeCreate 和 created 仍然支持,但如果主要使用 Composition API,可以只在 setup 中编写初始化逻辑。
(为什么组件实例在运行时创建:)
因为只有此时组件的所有选项和数据来源(如父组件传入的 props、应用提供的依赖等)都已确定,Vue 才能真正实例化组件并运行这些初始化逻辑。在编译时还没有具体的数据环境,无法生成真实的组件实例。此外,一个组件可能在运行时被创建多次(比如在一个列表里有多个同样的子组件,每个都需要独立的实例);编译时只能生成组件的“类”定义,而由这个定义在运行时按需创建出多个实例。
编译模板为渲染函数(运行时编译): 如果组件在编译时已经有了 render 函数(例如 SFC 预编译或组件选项里直接提供),Vue 将直接使用它。否则(比如传入了模板字符串,或使用包含运行时编译器的版本),Vue 会在 setupComponent 里调用编译器将模板转换为渲染函数。这个步骤实际上就是前面描述的编译过程在运行时的触发,通常通过调用 compile 或 compileToFunction 来得到渲染函数,然后赋给组件实例的 render 属性。在官方生命周期图中,这一步对应判断“是否有预编译模板”的分支,如果没有则“即时编译模板”【12†图】。编译完成后,组件实例已经准备好渲染。
初次渲染挂载: Vue 接下来会调用内部的 setupRenderEffect,为组件创建一个渲染副作用(effect)。这是一个响应式的副作用函数,它会执行组件的 render 函数并根据返回的虚拟 DOM 树进行实际 DOM 操作。在执行渲染函数时,Vue 会将组件实例设置为当前上下文,从而在渲染过程中触发依赖收集——即记录哪些响应式数据被用了,以便将来数据变更时精确触发这个组件重渲染。渲染函数产出的虚拟 DOM 会通过 Diff 算法与上一次结果比较(初次渲染则直接创建),并生成最小的 DOM 更新操作,将真实 DOM 树挂载出来。当所有子树都挂载完毕后,组件即进入 mounted 已挂载状态。
初次挂载流程中,Vue 将调用 Options API 的 beforeMount 钩子(在真实 DOM 插入之前)以及 mounted 钩子(在真实 DOM 挂载到页面后)【12†图】。这让开发者有机会在 DOM 创建前后执行自定义逻辑(例如操作未挂载时的 DOM 模板,或在组件挂载后访问 DOM元素等)。
更新和卸载: 组件进入运行状态后,其响应式状态发生变化时,会触发之前注册的渲染副作用重新执行。这会得到新的虚拟 DOM 树并与旧树进行高效地patch 更新,比对出最小差异并应用到真实 DOM。在重新渲染前,调用 beforeUpdate 钩子;更新后,调用 updated 钩子【12†图】。如果组件从界面上被移除,Vue 会调用 beforeUnmount 钩子,然后销毁组件实例并调用 unmounted 钩子,释放其响应式副作用等资源【12†图】。

上图展示了 Vue 组件实例的生命周期流程。从渲染器首次遇到组件节点开始,初始化组件实例(运行 Composition API 的 setup 和 Options API 的 beforeCreate),然后如果需要编译模板则进行编译,接着执行初次渲染并挂载 DOM,触发 mounted。组件运行过程中响应式数据变化会触发重新渲染和 updated,最终卸载时调用相应销毁钩子。
综上,组件实例的创建发生在运行时的挂载阶段。当编译完成后,Vue 持有的是组件的定义和渲染函数,就好比构造函数或类的定义;只有在运行时调用这个构造函数(即 createComponentInstance)才能生成具体的实例对象。运行时创建实例能够依据当前传入的 props、全局状态和父子关系正确地初始化组件,并启动响应式系统和生命周期钩子。这区别于编译时:编译时没有实际数据环境,不会产生真实的组件实例。Vue 在编译时尽可能优化了渲染所需的代码和提示,而将与数据、状态相关的工作留到运行时去完成。因此,组件实例总是在运行时创建,这也是框架灵活性的体现——同一套编译输出可以根据不同数据多次创建组件实例而各自独立运作。各个实例在运行时管理各自的响应式状态和 DOM 树,从而实现 Vue 的组合式用户界面。
参考资料:
Vue.js 官方文档:指南 – 渲染机制、生命周期钩子及生命周期图示【12†图】
《Making Vue 3》 – Evan You 对 Vue 3 内部优化的描述
《Vue.js The Glovo Tech Blog》 – 对 Vue 3 挂载过程和模板编译的深入解析
Vue 3 源码分析文章 – 组件挂载与实例化流程(包含关键函数 createComponentInstance 和 setupComponent 的讲解)



















暂无评论内容