异步更新队列与nextTick
Vue通过异步更新队列将Watcher去重批量执行,nextTick 基于微任务实现延迟回调到DOM更新后。
queueWatcher 入队
JavaScript
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// 正在flush时插入正确位置
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
通过 has 对象去重,同一Watcher同一tick只入队一次,flush时按ID排序保证顺序。
flushSchedulerQueue 批量执行
JavaScript
function flushSchedulerQueue() {
flushing = true
// 按ID排序(父组件watcher先于子组件)
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
const watcher = queue[index]
const id = watcher.id
has[id] = null
watcher.run()
}
// 重置状态
waiting = false
flushing = false
queue.length = 0
index = 0
}
遍历队列执行 watcher.run(),触发组件更新。
Watcher.run 执行
JavaScript
class Watcher {
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue)
}
}
}
get() {
pushTarget(this)
const value = this.getter.call(this.vm, this.vm)
return value
}
}
run() 重新执行getter获取新值,对比变化后调用回调。
nextTick 实现
JavaScript
const callbacks = []
let pending = false
function nextTick(cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) cb.call(ctx)
else if (_resolve) _resolve(ctx)
})
if (!pending) {
pending = true
timerFunc() // 触发异步任务
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
回调推入队列,同一tick多次调用只触发一次异步任务。
timerFunc 微任务降级
JavaScript
let timerFunc
// 1. Promise (微任务)
if (typeof Promise !== 'undefined') {
const p = Promise.resolve()
timerFunc = () => p.then(flushCallbacks)
}
// 2. MutationObserver (微任务)
else if (typeof MutationObserver !== 'undefined') {
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)
}
}
// 3. setTimeout (宏任务)
else {
timerFunc = () => setTimeout(flushCallbacks, 0)
}
优先微任务保证DOM更新前执行,降级到setTimeout。
flushCallbacks 执行回调
JavaScript
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
使用副本执行防止执行期间新回调加入导致无限循环。
更新流程示例
JavaScript
// 同步代码
this.count = 1 // 触发setter -> watcher入队
this.count = 2 // 去重,不入队
this.count = 3 // 去重,不入队
// DOM不会立即更新
console.log(this.$el.textContent) // 旧值
// 异步更新
// nextTick -> flushSchedulerQueue -> watcher.run() -> DOM更新
// nextTick回调
this.$nextTick(() => {
console.log(this.$el.textContent) // 新值
})
同一tick多次赋值只触发一次DOM更新,nextTick回调在更新后执行。
要点总结
queueWatcher通过has对象去重,同一Watcher同一tick只入队一次- flush时按ID排序保证父组件先于子组件更新
watcher.run()重新执行getter获取新值,对比变化调用回调nextTick将回调推入队列,同一tick多次调用只触发一次异步任务timerFunc优先微任务(Promise),降级到MutationObserver/setTimeoutflushCallbacks使用副本执行防止执行期间新回调加入
📝 发现内容有误?点击此处直接编辑