Vue 3 单文件组件加载顺序详解
在 Vue 3 的单文件组件(.vue 文件)中,<template>、<script> 和 <style> 三个部分的加载和执行顺序遵循明确的时间线。下面我通过示例详细说明:
📊 总体加载时间线
编译阶段:<template>解析 → <script>编译 → <style>提取
运行时:<script>执行 → 组件实例化 → 虚拟DOM创建 → 挂载DOM → 应用<style>
一、基础示例与分析
示例组件
vue
<!-- ExampleComponent.vue -->
<template>
<div class="container">
<h1>{{ title }}</h1>
<button @click="increment">点击: {{ count }}</button>
</div>
</template>
<script setup>
// 阶段1: <script>执行开始
console.log('1. <script> 开始执行')
import { ref, onBeforeMount, onMounted } from 'vue'
// 阶段2: 声明响应式数据
const title = ref('Vue 3 组件')
const count = ref(0)
// 阶段3: 定义方法
const increment = () => {
count.value++
console.log('点击触发,当前count:', count.value)
}
// 阶段4: 注册生命周期钩子
onBeforeMount(() => {
console.log('4. onBeforeMount: 组件挂载前')
console.log(' 容器是否存在:', !!document.querySelector('.container'))
})
onMounted(() => {
console.log('6. onMounted: 组件已挂载')
console.log(' 容器是否存在:', !!document.querySelector('.container'))
console.log(' 计算后颜色:', window.getComputedStyle(document.querySelector('.container')).color)
})
// 阶段5: 同步代码结束
console.log('2. <script> 同步代码执行完毕')
// 微任务检查
Promise.resolve().then(() => {
console.log('5. 微任务执行')
})
// 宏任务检查
setTimeout(() => {
console.log('7. 宏任务执行')
}, 0)
</script>
<style scoped>
.container {
color: blue;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
font-size: 24px;
transition: color 0.3s ease;
}
</style>
控制台输出顺序
1. <script> 开始执行
2. <script> 同步代码执行完毕
3. 模板编译为渲染函数
4. onBeforeMount: 组件挂载前
容器是否存在: false
5. 微任务执行
6. onMounted: 组件已挂载
容器是否存在: true
计算后颜色: rgb(0, 0, 238) // blue颜色值
7. 宏任务执行
二、详细执行阶段解析
阶段1:编译时处理(构建阶段)
javascript
// 在构建过程中(npm run build时):
1. 解析 .vue 文件
2. 提取 <template> 编译为渲染函数
3. 提取 <script> 转换为JavaScript
4. 提取 <style> 处理为CSS
5. 生成最终bundle文件
阶段2:运行时执行(浏览器中)
javascript
// 实际执行顺序:
// Part 1: Script执行阶段
① 执行 <script setup> 中的同步代码
- 导入依赖
- 声明响应式数据
- 定义方法
- 注册生命周期钩子
// Part 2: 挂载准备阶段
② 触发 onBeforeMount 钩子
- 此时DOM尚未创建
- 无法访问模板中的元素
// Part 3: 模板渲染阶段
③ 调用渲染函数,创建虚拟DOM
④ 虚拟DOM对比和更新
⑤ 创建实际DOM元素
⑥ 将DOM插入页面
// Part 4: 样式应用阶段
⑦ 应用<style>中的样式
- 如果是scoped样式,添加data-v-*属性
- 如果是全局样式,直接应用
- 计算最终样式
// Part 5: 挂载完成阶段
⑧ 触发 onMounted 钩子
- DOM完全可用
- 样式已计算完成
- 可执行DOM相关操作
三、父子组件加载顺序
父子组件示例
vue
<!-- ParentComponent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<ChildComponent />
</div>
</template>
<script setup>
import { onBeforeMount, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
console.log('Parent: 1. script开始')
onBeforeMount(() => {
console.log('Parent: 4. onBeforeMount')
})
onMounted(() => {
console.log('Parent: 7. onMounted')
})
console.log('Parent: 2. script结束')
</script>
vue
<!-- ChildComponent.vue -->
<template>
<div class="child">子组件</div>
</template>
<script setup>
import { onBeforeMount, onMounted } from 'vue'
console.log('Child: 1. script开始')
onBeforeMount(() => {
console.log('Child: 5. onBeforeMount')
})
onMounted(() => {
console.log('Child: 6. onMounted')
})
console.log('Child: 2. script结束')
</script>
控制台输出
Parent: 1. script开始
Parent: 2. script结束
Child: 1. script开始
Child: 2. script结束
3. 模板编译阶段
Parent: 4. onBeforeMount
Child: 5. onBeforeMount
Child: 6. onMounted
Parent: 7. onMounted
父子组件加载规则:
父组件<script> → 子组件<script> → 父组件onBeforeMount → 子组件onBeforeMount → 子组件onMounted → 父组件onMounted
四、样式加载的特殊性
样式应用时机验证
vue
<template>
<div ref="el" class="styled-box">样式测试</div>
</template>
<script setup>
import { ref, onBeforeMount, onMounted, nextTick } from 'vue'
const el = ref(null)
onBeforeMount(() => {
console.log('onBeforeMount - 样式未应用')
})
onMounted(async () => {
console.log('onMounted - 初始检查:')
const style = window.getComputedStyle(el.value)
console.log(' 颜色:', style.color)
console.log(' 宽度:', style.width)
// 等待一次更新循环
await nextTick()
console.log('nextTick后 - 最终检查:')
console.log(' 颜色:', window.getComputedStyle(el.value).color)
console.log(' 宽度:', window.getComputedStyle(el.value).width)
})
</script>
<style scoped>
.styled-box {
color: red;
width: 200px;
padding: 20px;
background: linear-gradient(45deg, #f3f3f3, #e0e0e0);
transition: all 0.3s ease;
}
</style>
关键发现:
- 在
onMounted中,样式已应用 但可能未完全计算 - 复杂样式(渐变、过渡)可能需要
nextTick()后才能完全生效 - Scoped样式在挂载时注入,全局样式可能更早加载
五、实际项目注意事项
1. DOM操作的正确时机
vue
<script setup>
import { onMounted, nextTick } from 'vue'
// ❌ 错误:在onMounted中立即获取元素尺寸
onMounted(() => {
const width = element.offsetWidth // 可能为0
})
// ✅ 正确:等待一次更新循环
onMounted(async () => {
await nextTick()
const width = element.offsetWidth // 正确的尺寸
})
</script>
2. 第三方库初始化的时机
vue
<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = ref(null)
onMounted(() => {
// ✅ 正确:在onMounted中初始化,此时DOM已存在
const chart = echarts.init(chartRef.value)
// 如果依赖样式计算,可以添加nextTick
nextTick(() => {
chart.resize() // 确保图表尺寸正确
})
})
</script>
3. 数据获取的优化
vue
<script setup>
import { onMounted, ref } from 'vue'
const data = ref(null)
const isLoading = ref(false)
// 方案A:在onMounted中获取
onMounted(async () => {
isLoading.value = true
data.value = await fetchData()
isLoading.value = false
})
// 方案B:立即开始,显示加载状态
const init = async () => {
isLoading.value = true
data.value = await fetchData()
isLoading.value = false
}
init() // 在setup中调用
</script>
六、总结与最佳实践
加载顺序总结
1. 编译阶段
- template编译为渲染函数
- script转换为JavaScript
- style处理为CSS
2. 运行时阶段
- 执行<script>同步代码
- 触发onBeforeMount
- 创建虚拟DOM → 真实DOM
- 应用<style>样式
- 触发onMounted
- 微任务执行
- 宏任务执行
3. 更新阶段
- 数据变化触发重新渲染
- 应用更新后的样式
最佳实践指南
- 数据初始化 :在
<script>中定义,在onMounted中填充 - DOM操作 :总是在
onMounted之后执行 - 样式依赖操作 :使用
nextTick()确保样式计算完成 - 第三方库 :在
onMounted中初始化 - 事件监听 :
onMounted中添加,onUnmounted中移除
记忆口诀
script先执行,template后渲染
样式最后加,挂载才完成
父子有顺序,先子后父行
操作要等待,nextTick最稳
理解这个加载顺序能帮助您避免常见的时序bug,编写更健壮的Vue 3组件。