主题
$nextTick
基础
vue 实现 dom 更新是异步完成的
vue
<ul ref="ul">
<li v-for="item in arr" :key="item">{{item}}</li>
</ul>
js
data(){
return {
arr: []
}
},
methods: {
add(){
this.arr.push( new Date().getTime() )
const ul = this.$refs.ul
console.log(ul.children.length) // 同步操作,此时视图还未更新,所以为 0
}
}
如果想在本次事件循环更新视图后,立即执行某些操作,可使用 $nextTick
js
// 用$nextTick回调函数就可以做到异步执行了
add(){
this.arr.push(new Date())
this.$nextTick(() => {
const ul = this.$refs.ul
console.log(ul.children.length) // 1
})
}
原理分析
vue 异步更新 dom 的原理(官方原话)
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
、MessageChannel
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替
vue 异步更新 dom 的降级策略:优先使用微任务(promise、mutationObserver),然后才会降级使用宏任务(setImmediate、MessageChannel、setTimeout)
验证猜想:
vue
<div ref="dom">{{ flag }}</div>
js
data() {
return {
flag: false
}
},
mounted() {
this.flag = true
// 第一步
setTimeout(() => { console.log(111) })
// 第二步
Promise.resolve().then(() => { console.log(222) })
// 第三步
this.$nextTick(() => {
console.log(this.$refs.dom)
console.log(333)
})
// 第四部
Promise.resolve().then(() => { console.log(444) })
}
会有如下打印
html
<div>true</div>
333
222
444
111
基于以上打印,得出下面结论
- chrome 浏览器环境下 nextTick 内部是执行了微任务的
- 同样是微任务,nextTick 执行的顺序第二步的 Promise。其实这个和改变数据触发watcher更新的先后有关,看下面一段的验证
js
data() {
return {
flag: false
}
},
mounted() {
// 第一步
setTimeout(() => { console.log(111) })
// 第二步
Promise.resolve().then(() => { console.log(222) })
// 第三步
this.flag = true
this.$nextTick(() => {
console.log(this.$refs.dom)
console.log(333)
})
// 第四部
Promise.resolve().then(() => { console.log(444) })
}
会有如下打印
html
222
<div>true</div>
333
444
111
当响应式数据发生变化,会触发它的 setter,从而通知 Dep 调用相关 watcher 的 update 函数,进行视图更新
js
update() {
// ...
/* 推送 watcher 对象到观察者队列中 */
queueWatcher(this)
}
js
queueWatcher(watcher) {
/* 获取watcher的id */
const id = watcher.id
/* 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验 */
if (has[id] == null) {
has[id] = true
if (!flushing) {
/*如果没有flush掉,直接push到队列中即可*/
queue.push(watcher)
} else {
let i = queue.length - 1
while (i >= 0 && queue[i].id > watcher.id) {
i--
}
queue.splice(Math.max(i, index) + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
可以看出,queueWatcher方法内部主要做的就是将watcher push到了queue队列当中
同时当waiting为false时,调用了一次 nextTick方法, 同时传入了一个参数 flushSchedulerQueue,其实这个参数,就是具体的队列更新函数,也就是说更新dom操作就是在这里面做的。而这个waiting状态的作用,很明显是为了保证nextTick(flushSchedulerQueue)只会执行一次
js
function flushSchedulerQueue() {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
}
nextTick 内部执行
js
/*
延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc
/*下一个tick时的回调*/
function nextTickHandler() {
/*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
pending = false
/*执行所有callback*/
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
/* 降级调用策略 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
/*使用Promise*/
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
}
} else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
/*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
/*使用setTimeout将回调推入任务队列尾部*/
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
/*
推送到队列中下一个tick时执行
cb 回调函数
ctx 上下文
*/
return function queueNextTick(cb, ctx) {
let _resolve
/*cb存到callbacks中*/
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve) => {
_resolve = resolve
})
}
}
})()
- 首先可以看出,nextTick 是一个立即执行函数,也就是说这个函数在定义的时候就已经自动执行一次了,即
return function queueNextTick
前面的代码已经执行过了 - 定义了一个函数timerFunc,这个函数决定了nextTick内部最终是执行了微任务,还是执行了宏任务
- 定义了一个函数nextTickHandler,就是把所有队列中的回调拿出来执行
- return了一个函数queueNextTick,我们平常调用的
this.$nextTick(cb)
及上面的nextTick(flushSchedulerQueue)
,实际上就是调用这个 queueNextTick
queueNextTick 做了什么
- 将传入进来的callback回调函数,push 到队列中
- 当pending为false时,调用了timerFunc函数,而timerFunc函数其实就是注册一个异步任务,执行队列里的回调
回到最初的代码,看代码怎么走的
js
data() {
return {
flag: false
}
},
mounted() {
this.flag = true
// 第一步
setTimeout(() => { console.log(111) })
// 第二步
Promise.resolve().then(() => { console.log(222) })
// 第三步
this.$nextTick(() => {
console.log(this.$refs.dom)
console.log(333)
})
// 第四部
Promise.resolve().then(() => { console.log(444) })
}
this.flag = true
执行,响应式数据发生变化,触发 update 方法update方法内执行了queueWatcher函数,将相关watcher push到queue队列中。并执行了 **nextTick(flushSchedulerQueue) **
nexTick内部代码执行,实际上是执行了queueNextTick,传入了一个flushSchedulerQueue函数,将这个函数加入到了callbacks数组中,此时数组中只有一个cb函数flushSchedulerQueue
pending状态初始为false,故执行了timerFunc
jsif (!pending) { pending = true timerFunc() }
timerFunc执行,会创建一个微任务,此时是微任务队列中的第一个任务
然后回到最初的代码的
setTimeout
,加入宏任务队列碰到第一个Promise,加入微任务队列,此时微任务队列中,有两个微任务
此时this.$nextTick()函数执行,相当于就是调用了queueNextTick,并传入了一个回调函数。因为我们已经执行过一次queueNextTick,所以pending为true,那么timerFunc是不是这个时候不会再执行了,而此时唯一做的操作就是将传入的回调函数加入到了callbacks数组当中
碰到第二个promise,加入微任务队列,此时微任务队列中,有三个微任务
当同步代码执行完成后,优先清空微任务队列,会先清空第一个微任务,也就是timeFunc内的那个微任务
则会更新dom,更新完 dom 后,执行我们传入的回调函数
。。。