生命周期钩子是 Vue 开发中不可或缺的工具。本章汇总各种实际应用场景,帮助你选择正确的钩子函数。
| 场景 | 推荐钩子 | 说明 |
|---|---|---|
| 发起异步请求 | created | 尽早获取数据 |
| 操作 DOM | mounted | DOM 已渲染 |
| 初始化第三方库 | mounted | 需要 DOM 存在 |
| 添加事件监听 | mounted | 需要访问元素 |
| 清除定时器 | beforeDestroy | 实例仍可用 |
| 移除事件监听 | beforeDestroy | 防止内存泄漏 |
| 销毁第三方库 | beforeDestroy | 释放资源 |
| 监听数据变化 | watch | 更精确控制 |
| DOM 更新后操作 | updated + $nextTick | 确保 DOM 已更新 |
export default {
data() {
return {
user: null,
loading: true,
error: null
}
},
created() {
this.fetchUser()
},
methods: {
async fetchUser() {
try {
const response = await api.getUser()
this.user = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}
}
}
当数据请求依赖 props 时,使用 watch:
export default {
props: {
userId: {
type: Number,
required: true
}
},
data() {
return {
user: null
}
},
created() {
this.fetchUser()
},
watch: {
userId() {
this.fetchUser()
}
},
methods: {
async fetchUser() {
this.user = await api.getUser(this.userId)
}
}
}
export default {
data() {
return {
user: null,
posts: null,
ready: false
}
},
async created() {
const [user, posts] = await Promise.all([
api.getUser(),
api.getPosts()
])
this.user = user
this.posts = posts
this.ready = true
}
}
export default {
data() {
return {
data: null,
timer: null
}
},
mounted() {
this.fetchData()
this.startPolling()
},
beforeDestroy() {
this.stopPolling()
},
methods: {
async fetchData() {
this.data = await api.getData()
},
startPolling() {
this.timer = setInterval(this.fetchData, 30000)
},
stopPolling() {
clearInterval(this.timer)
}
}
}
export default {
data() {
return {
width: 0,
height: 0
}
},
mounted() {
this.measureElement()
window.addEventListener('resize', this.measureElement)
},
beforeDestroy() {
window.removeEventListener('resize', this.measureElement)
},
methods: {
measureElement() {
const rect = this.$refs.container.getBoundingClientRect()
this.width = rect.width
this.height = rect.height
}
}
}
export default {
template: `
<input ref="input" v-model="value">
`,
props: {
autofocus: {
type: Boolean,
default: false
}
},
mounted() {
if (this.autofocus) {
this.$refs.input.focus()
}
}
}
export default {
props: {
scrollToBottom: Boolean
},
mounted() {
if (this.scrollToBottom) {
this.$nextTick(() => {
const container = this.$refs.container
container.scrollTop = container.scrollHeight
})
}
},
updated() {
if (this.scrollToBottom) {
this.$nextTick(() => {
const container = this.$refs.container
container.scrollTop = container.scrollHeight
})
}
}
}
export default {
template: '<div ref="chart" style="width: 100%; height: 400px;"></div>',
props: {
option: {
type: Object,
required: true
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.chart) {
this.chart.dispose()
}
},
watch: {
option: {
deep: true,
handler(newOption) {
if (this.chart) {
this.chart.setOption(newOption)
}
}
}
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chart)
this.chart.setOption(this.option)
},
handleResize() {
this.chart?.resize()
}
}
}
export default {
template: `
<ul ref="list">
<li v-for="item in items" :key="item.id" :data-id="item.id">
{{ item.name }}
</li>
</ul>
`,
props: {
items: Array
},
data() {
return {
sortable: null
}
},
mounted() {
this.initSortable()
},
beforeDestroy() {
if (this.sortable) {
this.sortable.destroy()
}
},
methods: {
initSortable() {
this.sortable = new Sortable(this.$refs.list, {
animation: 150,
onEnd: (evt) => {
const { oldIndex, newIndex } = evt
this.$emit('sort', { oldIndex, newIndex })
}
})
}
}
}
export default {
data() {
return {
showSearch: false
}
},
mounted() {
document.addEventListener('keydown', this.handleKeydown)
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeydown)
},
methods: {
handleKeydown(e) {
if (e.key === '/' && !this.showSearch) {
e.preventDefault()
this.showSearch = true
}
if (e.key === 'Escape' && this.showSearch) {
this.showSearch = false
}
}
}
}
export default {
data() {
return {
windowWidth: 0,
windowHeight: 0,
isMobile: false
}
},
mounted() {
this.updateWindowSize()
window.addEventListener('resize', this.updateWindowSize)
},
beforeDestroy() {
window.removeEventListener('resize', this.updateWindowSize)
},
methods: {
updateWindowSize() {
this.windowWidth = window.innerWidth
this.windowHeight = window.innerHeight
this.isMobile = this.windowWidth < 768
}
}
}
export default {
template: `
<div class="dropdown" v-if="isOpen">
<slot></slot>
</div>
`,
data() {
return {
isOpen: false
}
},
mounted() {
document.addEventListener('click', this.handleClickOutside)
},
beforeDestroy() {
document.removeEventListener('click', this.handleClickOutside)
},
methods: {
toggle() {
this.isOpen = !this.isOpen
},
handleClickOutside(e) {
if (!this.$el.contains(e.target)) {
this.isOpen = false
}
}
}
}
export default {
data() {
return {
countdown: 60,
timer: null
}
},
mounted() {
this.startCountdown()
},
beforeDestroy() {
this.stopCountdown()
},
methods: {
startCountdown() {
this.timer = setInterval(() => {
if (this.countdown > 0) {
this.countdown--
} else {
this.stopCountdown()
this.$emit('finish')
}
}, 1000)
},
stopCountdown() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
}
}
}
export default {
data() {
return {
socket: null,
messages: []
}
},
mounted() {
this.connect()
},
beforeDestroy() {
this.disconnect()
},
methods: {
connect() {
this.socket = new WebSocket(this.url)
this.socket.onopen = () => {
console.log('Connected')
}
this.socket.onmessage = (event) => {
this.messages.push(JSON.parse(event.data))
}
this.socket.onerror = (error) => {
console.error('WebSocket error:', error)
}
},
disconnect() {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
}
}
export default {
data() {
return {
abortController: null,
data: null
}
},
methods: {
async fetchData() {
this.abortController = new AbortController()
try {
const response = await fetch('/api/data', {
signal: this.abortController.signal
})
this.data = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error)
}
}
},
cancelRequest() {
if (this.abortController) {
this.abortController.abort()
}
}
},
beforeDestroy() {
this.cancelRequest()
}
}
export default {
template: `
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
`,
data() {
return {
currentTab: 'home'
}
}
}
Vue.component('home', {
template: '<div>Home: {{ count }}</div>',
data() {
return {
count: 0
}
},
activated() {
console.log('Home activated')
},
deactivated() {
console.log('Home deactivated')
}
})
export default {
data() {
return {
scrollTop: 0
}
},
activated() {
this.$nextTick(() => {
this.$refs.container.scrollTop = this.scrollTop
})
},
deactivated() {
this.scrollTop = this.$refs.container.scrollTop
}
}
new Vue({
el: '#app',
errorCaptured(err, vm, info) {
console.error('Error:', err)
console.error('Component:', vm)
console.error('Info:', info)
this.$notify({
type: 'error',
message: '发生错误,请稍后重试'
})
return false
}
})
export default {
data() {
return {
error: null
}
},
errorCaptured(err, vm, info) {
this.error = err.message
return false
}
}
1. 不要在 updated 中直接修改数据
updated() {
this.counter++
}
2. 不要忘记清理资源
mounted() {
this.timer = setInterval(this.poll, 5000)
}
3. 使用 $nextTick 确保 DOM 更新
this.items.push(item)
this.$nextTick(() => {
this.scrollToBottom()
})