更新阶段在数据变化时触发。当响应式数据发生变化,Vue 会重新渲染虚拟 DOM 并更新真实 DOM。这个过程涉及两个钩子:beforeUpdate 和 updated。
beforeUpdate 在数据变化后,DOM 更新之前调用。
数据变化
│
▼
触发 setter
│
▼
通知 watcher
│
▼
beforeUpdate ← 数据已更新,DOM 未更新
│
▼
重新渲染虚拟 DOM
│
▼
更新真实 DOM
│
▼
updated
<div id="app">
<p ref="text">{{ message }}</p>
<button @click="update">更新</button>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello'
},
methods: {
update() {
this.message = 'World'
}
},
beforeUpdate() {
console.log('beforeUpdate')
console.log('数据:', this.message)
console.log('DOM:', this.$refs.text.textContent)
}
})
</script>
点击按钮后输出:
beforeUpdate
数据: World
DOM: Hello
1. 获取更新前的 DOM 状态
beforeUpdate() {
this.oldScrollTop = this.$refs.container.scrollTop
}
2. 记录变化日志
beforeUpdate() {
console.log('数据即将更新:', JSON.stringify(this.$data))
}
updated 在数据变化导致的 DOM 更新完成后调用。
beforeUpdate
│
▼
重新渲染虚拟 DOM
│
▼
diff 算法比较
│
▼
更新真实 DOM
│
▼
updated ← DOM 已更新完成
new Vue({
el: '#app',
data: {
message: 'Hello'
},
methods: {
update() {
this.message = 'World'
}
},
updated() {
console.log('updated')
console.log('数据:', this.message)
console.log('DOM:', this.$refs.text.textContent)
}
})
点击按钮后输出:
updated
数据: World
DOM: World
1. DOM 更新后的操作
updated() {
this.scrollToBottom()
},
methods: {
scrollToBottom() {
const container = this.$refs.messages
container.scrollTop = container.scrollHeight
}
}
2. 第三方库同步更新
updated() {
if (this.chart) {
this.chart.update()
}
}
3. 检测元素尺寸变化
updated() {
const rect = this.$refs.element.getBoundingClientRect()
if (rect.height !== this.lastHeight) {
this.lastHeight = rect.height
this.$emit('resize', rect)
}
}
在 updated 中修改数据会导致无限循环:
updated() {
this.counter++
}
这会导致:
数据变化 → beforeUpdate → 更新 DOM → updated → 数据变化 → ...
如果需要在 updated 中修改数据,应该添加条件判断:
updated() {
if (this.needsUpdate) {
this.needsUpdate = false
this.doSomething()
}
}
或者使用 $nextTick:
updated() {
this.$nextTick(() => {
if (this.shouldUpdate) {
this.performUpdate()
}
})
}
$nextTick 和 updated 都在 DOM 更新后执行,但有关键区别:
| 特性 | updated | $nextTick |
|---|---|---|
| 触发条件 | 任意数据变化 | 手动调用 |
| 执行次数 | 每次更新都执行 | 只执行一次 |
| 使用场景 | 响应所有更新 | 特定更新后执行 |
很多时候,使用 $nextTick 比 updated 更合适:
methods: {
addItem() {
this.items.push({ id: Date.now(), text: 'New item' })
this.$nextTick(() => {
this.scrollToBottom()
})
}
}
父子组件更新时的执行顺序:
父 beforeUpdate
子 beforeUpdate
子 updated
父 updated
Vue.component('child', {
template: '<div>{{ value }}</div>',
props: ['value'],
beforeUpdate() { console.log('子 beforeUpdate') },
updated() { console.log('子 updated') }
})
new Vue({
template: '<child :value="message"></child>',
data: { message: 'Hello' },
beforeUpdate() { console.log('父 beforeUpdate') },
updated() { console.log('父 updated') },
methods: {
update() {
this.message = 'World'
}
}
}).$mount()
输出结果:
父 beforeUpdate
子 beforeUpdate
子 updated
父 updated
Vue.component('chat-window', {
template: `
<div class="chat-container" ref="container">
<div v-for="msg in messages" :key="msg.id" class="message">
{{ msg.text }}
</div>
</div>
`,
props: {
messages: Array
},
data() {
return {
userScrolled: false
}
},
mounted() {
this.$refs.container.addEventListener('scroll', this.handleScroll)
},
beforeDestroy() {
this.$refs.container.removeEventListener('scroll', this.handleScroll)
},
updated() {
if (!this.userScrolled) {
this.scrollToBottom()
}
},
methods: {
scrollToBottom() {
const container = this.$refs.container
container.scrollTop = container.scrollHeight
},
handleScroll() {
const container = this.$refs.container
const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50
this.userScrolled = !isAtBottom
}
}
})
Vue.component('virtual-list', {
template: `
<div class="virtual-list" ref="container" @scroll="handleScroll">
<div class="content" :style="{ height: totalHeight + 'px' }">
<div v-for="item in visibleItems" :key="item.id" ref="items">
<slot :item="item"></slot>
</div>
</div>
</div>
`,
props: {
items: Array,
itemHeight: Number
},
data() {
return {
startIndex: 0,
visibleCount: 10
}
},
computed: {
totalHeight() {
return this.items.length * this.itemHeight
},
visibleItems() {
return this.items.slice(
this.startIndex,
this.startIndex + this.visibleCount
)
}
},
updated() {
this.$nextTick(() => {
this.updateItemPositions()
})
},
methods: {
handleScroll() {
const scrollTop = this.$refs.container.scrollTop
this.startIndex = Math.floor(scrollTop / this.itemHeight)
},
updateItemPositions() {
const items = this.$refs.items
if (!items) return
items.forEach((item, index) => {
item.style.position = 'absolute'
item.style.top = (this.startIndex + index) * this.itemHeight + 'px'
})
}
}
})
Vue.component('sortable-table', {
template: `
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key" @click="sortBy(col.key)">
{{ col.label }}
<span v-if="sortKey === col.key" class="sort-indicator">
{{ sortOrder > 0 ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in sortedData" :key="row.id">
<td v-for="col in columns" :key="col.key">{{ row[col.key] }}</td>
</tr>
</tbody>
</table>
`,
props: {
columns: Array,
data: Array
},
data() {
return {
sortKey: null,
sortOrder: 1
}
},
computed: {
sortedData() {
if (!this.sortKey) return this.data
return [...this.data].sort((a, b) => {
const aVal = a[this.sortKey]
const bVal = b[this.sortKey]
return (aVal > bVal ? 1 : -1) * this.sortOrder
})
}
},
updated() {
this.highlightSortedColumn()
},
methods: {
sortBy(key) {
if (this.sortKey === key) {
this.sortOrder *= -1
} else {
this.sortKey = key
this.sortOrder = 1
}
},
highlightSortedColumn() {
const headers = this.$el.querySelectorAll('th')
headers.forEach((th, index) => {
if (this.columns[index].key === this.sortKey) {
th.classList.add('sorted')
} else {
th.classList.remove('sorted')
}
})
}
}
})
使用计算属性缓存避免重复计算:
computed: {
filteredItems() {
return this.items.filter(item => item.active)
}
}
不需要更新的内容使用 v-once:
<div v-once>
{{ staticContent }}
</div>
冻结不需要响应式的数据:
created() {
this.largeList = Object.freeze(largeDataArray)
}
1. 避免在 updated 中修改数据
updated() {
this.counter++
}
2. 使用 $nextTick 进行精确控制
this.items.push(newItem)
this.$nextTick(() => {
this.scrollToNew()
})
3. 添加条件判断避免重复执行
updated() {
if (this.pendingScroll) {
this.pendingScroll = false
this.scrollToBottom()
}
}
4. 考虑使用 watch 替代 updated
watch: {
items() {
this.$nextTick(() => {
this.scrollToBottom()
})
}
}