Vue.nextTick 原理与应用
nextTick 的实现原理、降级策略及常见使用场景
问题
Vue.nextTick 的原理是什么?它的 Microtask/Macrotask 降级策略是怎样的?有哪些应用场景?
解答
nextTick 是什么
nextTick 用于在 DOM 更新完成后执行回调。Vue 的数据变化到 DOM 更新是异步的,如果需要操作更新后的 DOM,就要用 nextTick。
this.message = 'updated'
// DOM 还没更新
console.log(this.$el.textContent) // 旧值
this.$nextTick(() => {
// DOM 已更新
console.log(this.$el.textContent) // 'updated'
})
Vue 2.x 降级策略
Vue 2.x 优先使用微任务,不支持时降级到宏任务:
// Vue 2.x nextTick 简化实现
const callbacks = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 降级策略
let timerFunc
if (typeof Promise !== 'undefined') {
// 1. 优先使用 Promise(微任务)
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof MutationObserver !== 'undefined') {
// 2. MutationObserver(微任务)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else if (typeof setImmediate !== 'undefined') {
// 3. setImmediate(宏任务,仅 IE/Node 支持)
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4. setTimeout(宏任务,兜底方案)
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
function nextTick(cb, ctx) {
callbacks.push(() => {
if (cb) {
cb.call(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}
Vue 3.x 实现
Vue 3 直接使用 Promise,不再做降级:
// Vue 3.x nextTick 实现
const resolvedPromise = Promise.resolve()
let currentFlushPromise = null
function nextTick(fn) {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(fn) : p
}
应用场景
1. 获取更新后的 DOM
export default {
data() {
return { list: [] }
},
methods: {
async addItem() {
this.list.push({ id: Date.now() })
// 等待 DOM 更新后滚动到底部
await this.$nextTick()
const container = this.$refs.container
container.scrollTop = container.scrollHeight
}
}
}
2. created 中操作 DOM
export default {
created() {
// created 时 DOM 还不存在
this.$nextTick(() => {
// 此时可以访问 DOM
this.$refs.input.focus()
})
}
}
3. 多次数据修改后获取最终 DOM
export default {
methods: {
updateMultiple() {
// 多次修改只会触发一次 DOM 更新
this.a = 1
this.b = 2
this.c = 3
this.$nextTick(() => {
// 获取所有修改后的 DOM 状态
console.log(this.$el.innerHTML)
})
}
}
}
4. 配合 v-if 使用
export default {
data() {
return { showInput: false }
},
methods: {
show() {
this.showInput = true
// v-if 切换后 DOM 才会渲染
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}
}
关键点
nextTick将回调延迟到 DOM 更新后执行- Vue 2.x 降级顺序:Promise → MutationObserver → setImmediate → setTimeout
- Vue 3.x 直接使用 Promise,不再降级
- 微任务比宏任务执行时机更早,能更快响应
- 同一事件循环内多次调用
nextTick,回调会合并到一个队列中批量执行
目录