文章目录
前言
Vue中nextTick的实现原理和对源码的分析。
一、nextTick是什么?
官方文档中,提到nextTick是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
在Vue开发时,有没有遇到这样的情况,在改了data里的数据后获取DOM时里边的值竟然还是原来的值,甚至在循环中,这次获取的DOM永远是上一次的状态,但页面明明都变了呀,但是在使用nextTick后这种情况的到解决,简直摸不到头脑。
不过,在查看nextTick源码,还有网上的相关资料后,豁然开朗。
有意思的是官方文档还标注了:
2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。
IE躺着也中枪,源码中确实也体现了官方的这句话。
二、为什么会出现这种情况
1.JS的运行机制?
神XX机制,我真的会谢,能用就行呗,哈哈哈哈,开玩笑,开玩笑。
说起JS运行机制,还真没了解过,趁这个机会,好好了解一下。
(1)JS运行机制流程
JS的执行竟然是单线程的,我不知道,这是为什么,可能是机制吧,他是靠事件循环来执行。具体流程如下:
- 在主线程运行时,会形成执行栈,栈中在执行过程中遇到任务源,将任务源中的回调函数加入到任务队列中
- 执行栈执行完成后,会将任务队列中的回调函数放入执行栈依次执行。
经过反复循环这两个操作,就变成了事件循环。
(2)同步、异步执行过程
同(1)中所讲一致,不过同步任务是直接放到执行栈中执行,异步任务只能看着,突然发现还有一个任务队列,异步任务赶紧把自己的任务给他,让任务队列在同步执行完成后喊他。
任务:
在任务里边也有分类,分为宏任务和微任务
宏任务:
- 在宏任务执行过程中,可以创建宏任务加到宏任务队列的最后等待下一次循环执行;也可以创建微任务,加入到本次宏任务下微任务队列的最后,会在本次宏任务结束后顺序执行。(每个宏任务都有个微任务队列)
- 在本次宏任务执行完成后,这时候才去执行宏任务下的微任务队列,微任务结束后,算一个task执行完成。
- 在一个task执行完成,页面会进行渲染,然后进入下一个task。(Event loop)
- 常见的宏任务:
setTimeout, setInterval, requestAnimationFrame, I/O, script标签内代码,setImmediate
等。
微任务:
- 微任务创建的宏任务,会被加入到宏任务队列的最后。
- 微任务创建的微任务会被加入到微任务队列的最后,微任务会在本次task中执行完成。
- 常见的微任务有:
Promise callback, Promise.then,MutationObserver
解释场景问题
到这,大家应该知道为啥获取DOM的状态是上一次的吧,是因为渲染DOM都是异步进行的,在更改数据时,Vue开启了事件队列,数据更改的任务被推进了任务队列,在此时依赖了DOM时获取的肯定是上一次DOM状态,因为那时候DOM还没到重新渲染,只不过这个时间太短,你感觉不出来。
三、nextTick源码分析
1.浏览器能力?
nextTick执行延迟回调,如何实现这个功能,这可离不开浏览器的能力,浏览器的能力决定了nextTick以什么样的方式去实现这个功能,说白了就是最大限度提升性能。
如何判断浏览器能力?
微任务的耗时是小于宏任务的
-
当然是if()喽,首先去判断支不支持Promise,在其中把回调函数队列给Promise.then,然后就创建了微任务,值得注意的是在IOS中支持Promise,但他队列不更新,所以要加个空的任务强制刷新下队列。
-
如果不支持Promise也没关系,他会去找有没有原生的MutationObserver的API(ES5中检测DOM节点变化的API),显然它创建的也是个微任务
-
实在不行就来到了setImmediate,IE可以直接跳过前两个直接来到这,因为自身条件不达标,从这个开始创建的是宏任务了。
-
最不推荐的就是setTimeout了,他是最慢的。
// 判断浏览器的能力:
// 顺序:Promise-->MutationObserver-->setImmediate-->setTimeout
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop) // ios具备promise但不会更新队列信息,所以强制加入一个空计时器,强制刷新。
}
isUsingMicroTask = true
} else if ( //接下来就是判断有没有原生的MutationObserver,用来检测DOM树变化的API。
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
// 靠MutationObserver中characterData属性去观察目标节点(textNode)下所有文本类型节点(即子代或后代)的文字变化
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 然后是setImmediate 前两个都是创建微任务,但从这个开始就是创建宏任务了
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后推荐的是setTimeout,宏任务间也是有差距的
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
2.回调队列函数
回调队列函数中很简单,上锁和循环执行回调队列,
// 执行回调队列函数
function flushCallbacks() {
pending = false //重置异步锁
// 将回调函数队列复制并清空回调函数队列。防止nextTick中套娃出现错误,防止在回调函数中调用nextTick时将其回调函数加入到这个队列中,也就是不会开启多个异步任务。
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
3.nextTick主体
- 将传进来的回调函数推入对调队列中,再判断异步锁是否上锁,如果未上锁就在上锁后执行异步函数创建异步任务等待同步任务执行完成。
- 如果没有回调,则在支持Promise的情况下返回一个Promise(IE无语)
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 将回调函数推入回调队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
// 发生错误时,回滚并调用全局API handleError获取错误信息
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果异步锁未上锁,则锁上异步锁,调用异步函数等待同步函数完成
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
// 如果没有回调并且支持Promise则返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
总结
以上就是今天记录的内容,本文对JS的运行机制和nextTick原理进行了分析,其次除了nextTick外,可以用$forceUpdate ,强制更新,他只会影响到实例本身和插入插槽内容的子组件。