准备
vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。
要完全理解nextTick
的实现需要有以下知识储备:
- JS的事件循环机制
- ES6中的
Promise
的使用方法及原理 - 事件循环中的微任务与宏任务
回顾
如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》
nextTick
的两种使用方法
在Vue中,nextTick
有两种风格的使用方法,一种是回调函数风格:
this.$nextTick(()=>console.log("nextTick"))
另外一种是Promise
风格的使用方法:
this.$nextTick().then(()=>console.log("nextTick"))
Tick
在上一篇我们了解到在执行queueWatcher
的时候,最终是用nextTick
来调用flushSchedulerQueue
方法的,那么这个nextTick
是何方神圣呢?先不要急,我们先了解下Tick
的概念:
JS是单线程执行的,当执行完当前线程时,会去事件按顺序先取出微任务执行,再取出宏任务执行。
一个Tick
的流程就是:主线程执行完毕后再将当前的所有微任务执行完。所以说宏任务是在下一个Tick
执行的。
所以见名知意,nextTick(flushSchedulerQueue)
意思就是指watcher
渲染DOM是在下一个Tick
执行的,也就是运行在宏任务中。
但其实为了渲染效率,尽快去渲染DOM,如果执行环境支持的话还是会运行在微任务中。
进入nextTick.js
看看Vue是怎么实现nextTick
方法的。
nextTick.js
const callbacks = []
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]()
}
}
第一部分,声明一个数组callbacks
,用于存放所有需要在下一个Tick
中执行的任务;声明一个变量pending
,默认为false
。
定义了一个方法flushCallbacks
,用于遍历callbacks
挨个执行里面的任务。
继续往下看
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
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)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
这一段很长,但是我们不用一行一行看,只要大概看一下就知道这边其实就是对执行环境支持的一些判断,一开始定义的timerFunc
就是决定需要用哪种方式、哪种风格去执行下一个Tick
的任务。这边就能看到我刚刚说的如果执行环境允许的话还是会放在微任务中去执行,如果执行环境不允许的话才会降级在宏任务中运行。
这边我就以浏览器为例,首选Promise
,以Promise
风格去实现nextTick
。
继续向下看:
export function nextTick (cb?: Function, ctx?: Object) {
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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
这边就是nextTick
的核心了。传入两个参数(注:如果是以Promise
去使用nextTick
的话,两个参数都不用传入:this.$nextTick().then(() => console.log("next tick"))
)。
定义一个_resolve
变量,这个方法的最后我们可以看到,_resolve
就是一个Promise
实例的resolve
回调。
然后是向callbacks
列表去添加一个匿名函数,这个匿名函数有一个判断:
- 如果有第一个参数(“普通风格调用”),就执行回调函数。
- 如果没有第一个参数(“
Promise
风格调用”),就执行他的resolve
回调。
然后判断当前有没有正在执行的任务,如果没有就将pending
至为true
,并调用timerFunc
。
timerFunc
前面说过了,这边以Promise
实现为例:
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
获取一个已经resolve
的Promise
实例,然后调用flushCallbacks
在下一个Tick
中执行所有的任务队列。
总结
经过上面的源码我们可以得知,Vue的DOM渲染过程是异步的,JS在主线程执行过程中会一个一个的收集nextTick
的任务,然后放在下一个Tick
中全部执行完毕,其中DOM渲染也在nextTick
的任务列表中。
附录
经过以上对nextTick
的学习,看看对nextTick
的理解是不是很透彻。
这边有一段有趣的代码,猜猜最后的输出是什么?为什么会是这样的结果?:
<template>
<div>
<div ref="div">{{ message }}</div>
<button @click="handler">toggle</button>
</div>
</template>
<script>
export default {
data: () => ({
message: 'hello'
}),
methods: {
handler() {
this.$nextTick(() => console.log(this.$refs.div.innerHTML));
this.message = 'world';
console.log(this.$refs.div.innerHTML);
this.$nextTick().then(() => console.log(this.$refs.div.innerHTML));
}
}
};
</script>