前言
由于 Vue 是异步更新 DOM 的,如果数据改变后直接执行某些动作,而此时 DOM 尚未更新,则会立即执行。 Vue提供了nextTick
,数据改变后立即调用。可以保证更新完成后传入的回调函数会被执行。
然后按照两个问题来探究nextTick的原理
- 调用 nextTick 时会发生什么?
- 如何检测DOM更新完成?
下一个Tick
全局API Vue.nextTick
和实例方法vm.$nextTick
实际上在内部调用了nextTick
函数。 nextTick
中的逻辑非常简单。它的作用是将传入的回调函数压入callbacks
队列(如果没有回调且支持Promise,则传入一个解决方案)然后判断pending
为假并调用timerFunc
。这就对了。
const callbacks = []
let pending = false
export function nextTick (cb, ctx) {
let _resolve
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
})
}
}
本段代码来自 https://www.codeqd.com/post/202309211.html
可见timerFunc是决定何时执行回调函数的关键。 pending
表示当前是否有异步任务正在执行。如果没有,请立即调用timerFunc
并将pending
更改为true
,以防止重复调用导致进程死锁。
调用 timerFunc
时,会创建一个异步任务。当异步任务完成后,会执行回调队列中的函数,并将pending
设置为false
,等待下一个nextTick
被调用。当 timerFunc
结束之前,重复调用 nextTick 只会触发执行一次。
如何在Vue中实现异步任务
看到这里,我迫不及待地想知道timerFunc是如何实现的,回调函数什么时候会被执行。别担心,你需要先了解一些必备知识,即浏览器Event Loop
事件循环事件循环
我们知道js是单线程的,同步任务会顺序执行。我们还知道,带有事件监听回调、setTimeout、Promise 等的异步任务不会立即执行。那么js引擎是如何决定这些异步任务何时被调用的呢?
消息队列
Javascript 运行时包含一个用于处理待处理消息的消息队列。当绑定到事件侦听器的事件被触发或添加 setTimeout 回调时,一条消息将被添加到队列中并等待。被处理。
任务队列
每条消息都有一个与其关联的回调函数,该回调函数被放置在任务队列中。每次都会从消息队列的头部开始处理消息,并执行关联的回调函数,直到回调函数执行完毕。 。
任务和微任务
如上所述,(宏)任务附加到消息中。消息处理完成后,就会执行相应的任务,比如事件触发的回调、使用setTimeout添加的任务等。
和微任务独立存储在微任务队列中。当(宏)任务开始执行时,新添加的微任务将被添加到微任务队列中。任务执行完成后,下一次迭代之前,微任务将依次执行,直到微任务队列为空。如果不断添加微任务,则处理将继续,直到微任务队列为空。因此,需要防止重复添加微任务而阻塞进程。
(宏)任务:由事件监听器、setTimeout、setInterval 触发的回调
微任务:Promise、queueMicrotask、MutationObserver
事件循环概述
结合以上三个概念,可以总结出这些步骤
- 当消息添加到消息队列时,其对应的任务也会添加到任务队列中。当js引擎空闲时,它会取出一条消息并开始处理。后面的消息必须等待前面的消息执行完成才能处理
- 当消息被处理时,其对应的任务也会从任务队列中移除。在此期间,新添加的微任务,例如创建质押,将会被添加到微任务队列中
- 等到任务执行完成,会依次执行微任务,直到微任务队列为空
- 开始处理下一条消息,重复步骤123,这是事件循环
定时器功能原理
了解了什么是事件循环后,我们再来看看timerFunc的原理以及nextTick回调函数的执行时间。
从源码中可以看到Vue在创建异步任务时做了很多兼容处理。我们尝试使用 Promise
、MutationObserver
、setImmediate
、setTimeout
来创建异步任务
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// ...
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// ...
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// ...
} else {
// ...
}
本段代码来自 https://www.codeqd.com/post/202309211.html
答应.然后
首先确定Promise是否原生支持。 Native Promise.then
是一个比(宏)任务优先级更高的微任务。调用timerFunc
会添加一个微任务,并等到DOM更新完成后才会开始下一个事件循环flushCallbacks
将被执行
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// iOS 中奇怪的bug,微任务入列了却没有刷新,直到浏览器需要处理其他一些工作,如定时器,这里用来强制刷新队列
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
本段代码来自 https://www.codeqd.com/post/202309211.html
突变观察者
MutationObserver
接收回调函数,使用 new 关键字创建并返回一个实例对象,当指定的 DOM 更改时将调用该实例对象。
MutationObserver
虽然指定了DOM改变时触发,但只是添加了一个微任务,并不会立即执行。相反,直到所有 DOM 更新完成后才会执行。因此,Vue在这里只需要创建一个结构体即可。点击更改内容即可监控。
这里非常巧妙地使用counter = (counter + 1) % 2
来使计数器在 0/1 之间切换。调用timerFunc将会改变textNode的内容。等待监听的textNode发生变化,就会添加一个微任务。当 DOM 更新完成时,调用。
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
本段代码来自 https://www.codeqd.com/post/202309211.html
立即设置
此方法用于将一些长时间运行的操作放入回调函数中。该回调函数将在浏览器完成其他后续语句后立即执行。
仅 IE10+ 支持,是一个(宏)任务
timerFunc = () => {
setImmediate(flushCallbacks)
}
本段代码来自 https://www.codeqd.com/post/202309211.html
设置超时
是另一种选择。如果以上都不支持,则使用setTimeout
创建异步任务,这是一个(宏)任务
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
本段代码来自 https://www.codeqd.com/post/202309211.html
flushCallbacks
V 上面可以看到,Vue 尝试使用Promise.then
、MutationObserver
、setImmediate
、setTimeout
来创建异步任务, 将在 DOM 完成后执行。
flushCallbacks
先将pending改为false,等待下一次调用timerFunc
,然后执行回调函数队列,将回调函数一一取出来执行。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
本段代码来自 https://www.codeqd.com/post/202309211.html
总结
由于Vue异步更新DOM,nextTick
必须维护一个回调函数队列并等待正确的时间来执行回调函数。这次使用了事件循环机制。当Vue进行异步更新时,Vue更新完成后会处理新增的异步任务,并依次执行回调。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。