【源码】nextTick 源码解析
Opened this issue · 0 comments
【源码】nextTick 源码解析
前言
我们知道Vue中数据更新是异步的,如果我们在修改数据之后获取DOM中对应的数据,是无法获取到最新数据来进行相关操作,但是我们可以通过nextTick
这个机制来实现相关需求(本文不会涉及nextTick
的基础用法)
nextTick源码解析
我们先来看下nextTick
在源码中的定义(代码有省略)
// vue/src/core/util/next-tick.js
const callbacks = []
let pending = false
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => { // 将传入的回调保存到数组callbacks中
cb.call(ctx)
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们可以发现我们使用this.$nextTick
的时候传入的回调函数会保存在一个数组callbacks
中,然后通过pending
控制timerFunc
函数在某个时机执行
那我们接下来看下timerFunc
函数做了什么?
// vue/src/core/util/next-tick.js
function flushCallbacks () { // 将callbacks中的全部回调函数拷贝一份,然后依次执行
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' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
我们可以发现代码中生成了timerFunc
函数,然后把回调作为microTask或macroTask参与到事件循环中来, 并且依次为promise
-> MutationObserver
-> setImmediate
-> setTimeout
这样的顺序进行降级,并且通过flushCallbacks
方法将callbacks
中的全部回调拷贝一份,然后依次执行
nextTick流程梳理
虽然是读懂了nextTick
的源码,但是我们对nextTick
如果实现对应功能,为什么可以在数据异步更新的机制下获取到最新DOM还是不了解,我们现在来梳理一下这个流程
首先数据更新离不开Watcher
, 我们看下Watcher
中的这段代码
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}
因为sync属性默认为false
, 所以数据更新是异步的,我们继续看下queueWatcher
方法
/*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
/*获取watcher的id*/
const id = watcher.id
/*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
if (has[id] == null) {
has[id] = true
if (!flushing) {
/*如果没有flush掉,直接push到队列中即可*/
queue.push(watcher)
} else {
...
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
我们只需要关心最后一段代码nextTick(flushSchedulerQueue)
就好了
flushSchedulerQueue
函数的作用主要是执行视图更新的操作,它会把queue中所有的watcher取出来并执行相应的视图更新。
然后通过我们对nextTick
源码的理解,flushSchedulerQueue
方法会push到一个数组callbacks
中,如
this.name = 123
this.$nextTick(cb)
此时的数组callbacks
中如下, flushSchedulerQueue
为第一位
[flushSchedulerQueue, cb]
然后将数组callbacks
中的回调函数按照promise
-> MutationObserver
-> setImmediate
-> setTimeout
的顺序包装成微任务或者宏任务,最后赋值给timerFunc
,最后执行
这里就涉及到了事件环相关的知识点了
同步任务执行完之后会执行异步任务,在异步任务中先执行一个宏任务,然后清空微任务队列,然后进行GUI渲染视图
也就是我们通过将timerFunc
包装成异步任务,然后我们的nexttick
中传入的回调会在flushSchedulerQueue
执行之后执行,所以我们在回调中是可以获取到最新的DOM的,不同在于如果timerFunc
如果是微任务的话,浏览器把DOM更新的操作放在Tick执行microTask的阶段来完成,相比使用宏任务生成的一个macroTask会少一次UI的渲染。
总结
nextTick源码做了什么
nextTick
函数其实做了两件事情,一是生成一个timerFunc
,把回调作为microTask或macroTask参与到事件循环中来。二是把回调函数放入一个callbacks
队列,等待适当的时机执行。(这个时机和timerFunc
不同的实现有关)
为什么nextTick可以获取到最新DOM
因为在数据更新的时候会调用nexttick
方法,将更新视图的flushSchedulerQueue
方法作为第一位放入callbacks
中,依次遍历callbacks
队列的时候flushSchedulerQueue
先执行,所以后序的回调中因为视图已经更新了,所以可以获取最新的DOM
为什么说DOM更新是异步的
因为Vue源码中将视图更新的方法flushSchedulerQueue
通过nexttick
来调用,所以最后会被包装成宏任务或者微任务,利用事件环的概念,如果宏任务的话会在下一个tick中执行,如果是微任务的话会在当前tick中执行