1.在vue(2.7.4)中$nextTick的实现只有几十行代码,但是承担了vue中数据异步刷新的重要任务,这个也是面试题中经常会考察的一个vue细节点之一。
在使用nextTick时,你是不是也有下面的问题:
this.$nextTick(() => {})和this.$nextTick().then(() => {})这两种写法,在使用上有什么区别?- vue中的
$nextTick和node中的nextTick(如果熟悉node的同学)有什么区别? - 面试中也经常会被问到
$nextTick的执行时机或者底层实现是什么? - ...(欢迎评论补充)
带着这几个问题,我们话不多说,一起来学习一下源码:
1. 调用nextTick
let funA = () => {
console.log('handle nextTick');
};
this.$nextTick(funA)
上面的代码可以看到,我们期望vue能在下次更新(姑且先这么描述吧)时异步的调用我们的funA,此时$nextTick做了什么事情呢?,把大象装进冰箱的第一步,就是先打开冰箱,我们看看调用$nextTick时先做了什么:
查看代码发现:
Vue.prototype.$nextTick = function (fn: (...args: any[]) => any) {
return nextTick(fn, this)
}
而nextTick如下:
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 第一部分
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 第二部分
if (!pending) {
pending = true
timerFunc()
}
// 第三部分
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
看着代码挺简单的,也确实挺简单的,大致可以分为三块逻辑:
1.1 向队列中push一个方法
队列const callbacks: Array<Function> = [],大家应该也能想到,nextTick 不是一个同步方法,你可以同时调用多次nextTick,比如:
let funA = () => {
console.log('handle nextTick A');
};
let funB = () => {
console.log('handle nextTick B');
};
this.$nextTick(funA)
this.$nextTick(funB)
此时代码执行结束之后,funA和funB并未执行,而是被push到了callbacks队列中,做了一个临时的混存,等待调用。 这里逻辑比较简单:
if (cb) { // 这里就不用多说了
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) { // 这里需要注意
_resolve(ctx)
}
从上面的代码我们需要注意一点,当function cb没有传递时,即this.$nextTick()进行调用时怎么处理。可以看到代码中当调用没有穿刺cb参数时,同时_resolve存在时,调用之。
这类可以直接看第三部分代码:
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
意思是当cb没有传,同时当前运行环境有Promise时,_resolve 赋值为promise的resolve,因为上方的callbacks只是push了方法,所以只会在执行时才会进行check,此时第三部分代码已经执行完成。
1.2 触发方法执行
到关键了,上面吧啦了这么多,没啥核心,现在到核心了,timerFunc()的方法的实现:
这里我们有使用微任务的异步延迟包装器。在2.5版本中,使用了(宏)任务(与微任务相结合)。然而,当状态在重新绘制之前发生更改时(例如#6813,在转换中),它会出现一些的问题。此外,在事件处理程序中使用(宏)任务会导致一些无法规避的奇怪行为(例如#7109、#7153、#7546、#7834、#8109)。所以我们现在再次在各处使用微任务。这种折衷的一个主要缺点是,在一些情况下微任务的优先级太高,并且在假定顺序事件之间(例如#4521、#6690,它们有变通方法),或者甚至在同一事件的冒泡之间(#6566)触发。
所以在vue2.5之后慢慢的nextTick由宏任务和微任务一起,逐步转变为微任务为主的的设计:
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) ||
// PhantomJS and iOS 7.x
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)
}
}
来吧,一步步对代码进行拆分:
1.2.1 当promise存在时
这里vue对于promise的判断使用的是typeof Promise !== 'undefined'和 typeof Ctor === 'function' && /native code/.test(Ctor.toString()),这一点学习了(别嫌啰嗦,既然读源码就是要看细节)。
对于promise存在的环境,直接在promise的then方法中调用flushCallbacks:
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
这里可以看到在vue中,会优先选择promise来实现nextTick,同时在代码中可以看到有then之后执行了:
if (isIOS) setTimeout(noop)
对此vue的注释为:
当在触摸事件处理程序中触发时,它在iOS>=9.3.3中的UIWebView中会出现严重错误,在有问题的UIWebViews中,Promise.then不会完全崩溃,但它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要做一些其他工作,例如处理计时器。因此,我们可以通过添加一个空计时器来“强制”刷新微任务队列。
此处的noop,意为no operation,是一个空函数,只是为了给setTimeout传参。
同时在最后将是否使用微任务的标识置成true:
isUsingMicroTask = true
这个在vue的event会使用到,这里不再展开,只需知道这个是标志vue执行任务的机制。
1.2.2不是IE并且MutationObserver可用时
相对于Promise,MutationObserver有更广泛的支持(当然了,IE另说),同时MutationObserver本质上也是微任务的实现方式,所以在不支持Promise的环境中,如果有MutationObserver,肯定是首选:
!isIE
typeof MutationObserver !== 'undefined'
isNative(MutationObserver
MutationObserver.toString() === '[object MutationObserverConstructor]' // PhantomJS and iOS 7.x
判断条件也比Promise多一个,看起来是在PhantomJS and iOS 7.x中 ,MutationObserver和其他环境不一样。
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
看到这里,如果没有了解过MutationObserver的同学可能看的不是特别懂,没关系,我们一起来看一下例子:
// 选择需要观察变动的节点
const targetNode = document.getElementById("some-id");
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type === "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
observer.disconnect();
所以vue的代码的意思很好理解啦
const observer = new MutationObserver(flushCallbacks),将callback的执行函数作为MutationObserver的回调observer.observe(textNode, { characterData: true }),观察节点textNode,方式是当textNode的值发生改变时触发上方的回调timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) },上方调用timerFunc时,实际是更新textNode的值,接着触发MutationObserver的回调
其中有一点需要注意的是:
MutationObserver也是微任务 isUsingMicroTask = true
1.2.3 使用setImmediate
和上面一样,首先判断环境支持 setImmediate:
typeof setImmediate !== 'undefined' && isNative(setImmediate)
为什么要用setImmediate呢,可以查看setImmediate和setTimeout的区别:
setTimeout 用于安排在一定延迟后执行的回调函数,但不保证立即执行。 setImmediate 用于安排尽快执行的回调函数,在I/O操作后执行。
所以此时直接使用:
timerFunc = () => {
setImmediate(flushCallbacks)
}
1.2.4 使用setTimeout
最后的兜底方案:
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
此时这里的setImmediate和setTimeout都是宏任务。
1.2.5 总结
这里就是nextTick的核心逻辑了,四种不同环境下,不同的实现方式
1.3 队列执行
这里就是按照队列顺序执行回调:
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
这里需要注意的是,这里有一个pending实现的锁机制,上面我们看到在调用timerFunc之前置成true,直到这里才设置成false。
到这里代码解读完成,还是比较简单吧,这里附上源码学习一下:
// 是否使用微任务
export let isUsingMicroTask = false
// 存储nextTick异步任务的队列
const callbacks: Array<Function> = []
// 状态位
let pending = false
// 刷新重制任务队列
function flushCallbacks() {
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) ||
// PhantomJS and iOS 7.x
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)
}
}
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
* @internal
*/
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
2. 总结
现在可以回答一下上面的问题:
this.$nextTick(() => {})和this.$nextTick().then(() => {})这两种写法,在使用上有什么区别?
这么写的前提是环境支持promise的情况,这里要分两种情况:
- 如果环境支持promise,则这两种写法是两个任务的周期执行,手撸promise(超详细)
- 如果不支持promise,这种情况就会报错
- vue中的
$nextTick和node中的nextTick(如果熟悉node的同学)有什么区别?
这是两个完全不同的东西,一个是node环境原生支持的方法,另一个是vue自己实现类似于前者效果的方法
- 面试中也经常会被问到
$nextTick的执行时机或者底层实现是什么?
本文就是最好的回答