Vue.js 生命周期
什么是生命周期?
生命周期是指 Vue 组件从创建到销毁的整个过程。在这个过程中,Vue 提供了一系列钩子函数,让我们可以在特定阶段执行代码。
Vue 3 生命周期钩子
组合式 API 生命周期钩子
在 Vue 3 的 <script setup> 中,生命周期钩子需要从 Vue 中导入:
vue
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
onMounted(() => {
console.log('组件已挂载')
})
</script>生命周期图示
创建阶段
↓
setup() - 组件创建时执行
↓
onBeforeMount() - 挂载前
↓
onMounted() - 挂载后(可以访问 DOM)
↓
更新阶段(数据变化时)
↓
onBeforeUpdate() - 更新前
↓
onUpdated() - 更新后
↓
销毁阶段
↓
onBeforeUnmount() - 卸载前
↓
onUnmounted() - 卸载后各个生命周期钩子详解
onMounted(挂载后)
组件挂载到 DOM 后调用,此时可以访问 DOM 元素。
vue
<script setup>
import { ref, onMounted } from 'vue'
const message = ref('Hello')
const elementRef = ref(null)
onMounted(() => {
console.log('组件已挂载')
console.log('DOM 元素:', elementRef.value)
// 适合进行:
// - DOM 操作
// - 发起 API 请求
// - 初始化第三方库
})
</script>
<template>
<div ref="elementRef">{{ message }}</div>
</template>onBeforeMount(挂载前)
在组件挂载到 DOM 之前调用。
vue
<script setup>
import { onBeforeMount } from 'vue'
onBeforeMount(() => {
console.log('即将挂载组件')
// 此时还不能访问 DOM
})
</script>onUpdated(更新后)
响应式数据变化导致 DOM 更新后调用。
vue
<script setup>
import { ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
console.log('组件已更新,count:', count.value)
// 注意:避免在这里修改状态,可能导致无限循环
})
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="count++">增加</button>
</div>
</template>onBeforeUpdate(更新前)
在响应式数据变化后、DOM 更新前调用。
vue
<script setup>
import { ref, onBeforeUpdate } from 'vue'
const count = ref(0)
onBeforeUpdate(() => {
console.log('即将更新 DOM')
})
</script>onUnmounted(卸载后)
组件卸载后调用,用于清理工作。
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
let timer = null
onMounted(() => {
timer = setInterval(() => {
console.log('定时器运行中...')
}, 1000)
})
onUnmounted(() => {
console.log('组件已卸载')
// 清理定时器
if (timer) {
clearInterval(timer)
}
// 适合进行:
// - 清理定时器
// - 取消事件监听
// - 取消网络请求
})
</script>onBeforeUnmount(卸载前)
组件卸载前调用。
vue
<script setup>
import { onBeforeUnmount } from 'vue'
onBeforeUnmount(() => {
console.log('即将卸载组件')
// 可以在这里进行一些清理工作
})
</script>实战示例:数据加载
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const data = ref(null)
const loading = ref(true)
const error = ref(null)
let abortController = null
async function fetchData() {
loading.value = true
error.value = null
// 创建 AbortController 用于取消请求
abortController = new AbortController()
try {
const response = await fetch('https://api.example.com/data', {
signal: abortController.signal
})
data.value = await response.json()
} catch (err) {
if (err.name !== 'AbortError') {
error.value = err.message
}
} finally {
loading.value = false
}
}
onMounted(() => {
console.log('组件挂载,开始加载数据')
fetchData()
})
onUnmounted(() => {
console.log('组件卸载,取消请求')
// 取消未完成的请求
if (abortController) {
abortController.abort()
}
})
</script>
<template>
<div class="data-container">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="error" class="error">
<p>❌ 加载失败:{{ error }}</p>
<button @click="fetchData">重试</button>
</div>
<div v-else class="content">
<pre>{{ data }}</pre>
</div>
</div>
</template>
<style scoped>
.data-container {
padding: 20px;
max-width: 600px;
margin: 0 auto;
}
.loading {
text-align: center;
padding: 40px;
}
.spinner {
width: 40px;
height: 40px;
margin: 0 auto 20px;
border: 4px solid #f3f3f3;
border-top: 4px solid #42b983;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
text-align: center;
color: #f56c6c;
padding: 20px;
}
button {
padding: 8px 16px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>实战示例:定时器
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const currentTime = ref(new Date())
const count = ref(0)
let timeInterval = null
let countInterval = null
onMounted(() => {
console.log('启动定时器')
// 更新时间
timeInterval = setInterval(() => {
currentTime.value = new Date()
}, 1000)
// 计数器
countInterval = setInterval(() => {
count.value++
}, 1000)
})
onUnmounted(() => {
console.log('清理定时器')
if (timeInterval) {
clearInterval(timeInterval)
}
if (countInterval) {
clearInterval(countInterval)
}
})
function formatTime(date) {
return date.toLocaleTimeString('zh-CN')
}
</script>
<template>
<div class="timer-container">
<h2>实时时钟</h2>
<div class="time">{{ formatTime(currentTime) }}</div>
<div class="count">运行时间:{{ count }} 秒</div>
</div>
</template>
<style scoped>
.timer-container {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
max-width: 400px;
margin: 20px auto;
}
.time {
font-size: 48px;
font-weight: bold;
margin: 20px 0;
font-family: 'Courier New', monospace;
}
.count {
font-size: 18px;
opacity: 0.9;
}
</style>实战示例:事件监听
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
const mouseX = ref(0)
const mouseY = ref(0)
const scrollY = ref(0)
function handleResize() {
windowWidth.value = window.innerWidth
windowHeight.value = window.innerHeight
}
function handleMouseMove(event) {
mouseX.value = event.clientX
mouseY.value = event.clientY
}
function handleScroll() {
scrollY.value = window.scrollY
}
onMounted(() => {
console.log('添加事件监听')
window.addEventListener('resize', handleResize)
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
console.log('移除事件监听')
window.removeEventListener('resize', handleResize)
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('scroll', handleScroll)
})
</script>
<template>
<div class="event-monitor">
<h2>事件监控</h2>
<div class="info-grid">
<div class="info-card">
<h3>窗口尺寸</h3>
<p>{{ windowWidth }} × {{ windowHeight }}</p>
</div>
<div class="info-card">
<h3>鼠标位置</h3>
<p>X: {{ mouseX }}, Y: {{ mouseY }}</p>
</div>
<div class="info-card">
<h3>滚动位置</h3>
<p>{{ scrollY }}px</p>
</div>
</div>
</div>
</template>
<style scoped>
.event-monitor {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.info-card {
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
text-align: center;
}
.info-card h3 {
margin: 0 0 10px 0;
color: #42b983;
}
.info-card p {
font-size: 18px;
font-weight: bold;
margin: 0;
}
</style>实战示例:第三方库集成
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const chartContainer = ref(null)
let chartInstance = null
onMounted(() => {
console.log('初始化图表')
// 模拟初始化第三方图表库
if (chartContainer.value) {
// 实际使用时,这里会是 Chart.js、ECharts 等库的初始化代码
chartInstance = {
render: () => {
chartContainer.value.innerHTML = `
<div style="padding: 40px; text-align: center; background: #f0f0f0; border-radius: 8px;">
<h3>图表已初始化</h3>
<p>这里会显示实际的图表</p>
</div>
`
},
destroy: () => {
console.log('销毁图表实例')
}
}
chartInstance.render()
}
})
onUnmounted(() => {
console.log('清理图表')
// 销毁图表实例,释放资源
if (chartInstance) {
chartInstance.destroy()
chartInstance = null
}
})
</script>
<template>
<div class="chart-wrapper">
<h2>图表示例</h2>
<div ref="chartContainer" class="chart-container"></div>
</div>
</template>
<style scoped>
.chart-wrapper {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.chart-container {
min-height: 400px;
margin-top: 20px;
}
</style>生命周期最佳实践
1. 在 onMounted 中进行 DOM 操作
vue
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
onMounted(() => {
// ✅ 正确:在 onMounted 中访问 DOM
inputRef.value?.focus()
})
// ❌ 错误:在 setup 中直接访问 DOM(此时 DOM 还未创建)
// inputRef.value?.focus()
</script>
<template>
<input ref="inputRef" placeholder="自动聚焦" />
</template>2. 清理副作用
vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
let subscription = null
onMounted(() => {
// 订阅某个服务
subscription = someService.subscribe(data => {
console.log(data)
})
})
onUnmounted(() => {
// ✅ 清理订阅
if (subscription) {
subscription.unsubscribe()
}
})
</script>3. 避免在 onUpdated 中修改状态
vue
<script setup>
import { ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
// ❌ 错误:可能导致无限循环
// count.value++
// ✅ 正确:只读取状态或执行不会触发更新的操作
console.log('count 已更新:', count.value)
})
</script>4. 使用组合式函数封装生命周期逻辑
vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 可复用的组合式函数
function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', update)
})
onUnmounted(() => {
window.removeEventListener('resize', update)
})
return { width, height }
}
// 使用组合式函数
const { width, height } = useWindowSize()
</script>
<template>
<div>窗口尺寸:{{ width }} × {{ height }}</div>
</template>选项式 API vs 组合式 API
选项式 API(Vue 2 风格)
vue
<script>
export default {
data() {
return {
count: 0
}
},
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() {
console.log('beforeUnmount')
},
unmounted() {
console.log('unmounted')
}
}
</script>组合式 API(Vue 3 推荐)
vue
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
const count = ref(0)
// setup() 相当于 beforeCreate 和 created
console.log('setup')
onBeforeMount(() => console.log('onBeforeMount'))
onMounted(() => console.log('onMounted'))
onBeforeUpdate(() => console.log('onBeforeUpdate'))
onUpdated(() => console.log('onUpdated'))
onBeforeUnmount(() => console.log('onBeforeUnmount'))
onUnmounted(() => console.log('onUnmounted'))
</script>总结
- onMounted:组件挂载后,适合 DOM 操作、API 请求、初始化第三方库
- onUnmounted:组件卸载后,适合清理定时器、事件监听、取消请求
- onUpdated:数据更新后,避免在此修改状态
- setup():相当于 beforeCreate 和 created
- 使用组合式函数封装可复用的生命周期逻辑
- 始终记得清理副作用,避免内存泄漏
下一步
接下来学习 动态组件,了解如何动态切换组件。