前篇
上面文章讲到了Watcher && Scheduler,在queueWatcher函数里面会通过下面的方式来调用:
nextTick(flushSchedulerQueue)
nextTick是Vue里面一个比较核心的概念;不过在讲nextTick之前就必须要讲到JavaScript的运行机制和任务队列。
JavaScript的运行机制
众所周知,浏览器的脚本语言是JavaScript,这个语言最大的特点就是单线程,也就是在同一时间只能干一件事情。
为什么是单线程的呢?假定有两个线程,一个操作dom,一个删除dom,岂不就乱套了~
当然为了充分利用CPU,Html5提出了web worker,允许开发人员创建多个线程,但是子线程完全受主线程控制,但是不得操作DOM,这也是遵循了单线程的标准。
单线程呢,也就意味着所有的任务,都需要排队运行,一个任务运行结束后,才会去执行下一个任务;
熟悉JavaScript的开发人员都明白,有异步回调这个概念,也就是说会挂起等待中的任务,去执行下一个任务,等回调回来再去执行被挂起的任务。
综上所述,任务分为两种,一个是同步任务(synchronous,简称sync),一个是异步任务(asynchronous,简称async)。
- 同步任务指的是,在主线程上面排队执行的任务,一个任务的结束,才能执行下一个任务;
- 异步任务指的是,不在主线程上面的任务,而是在任务队列中,主线程执行完成后询问任务队列,从任务队列中取的一个任务,放到主线程中执行。
所以简要图示一下,就是这样的:
主进程会不断重复获取步骤,执行完一个qtask,则继续询问qtask任务队列,获取qtask,放到主线程来执行。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制
任务队列
任务队列,也就是异步任务的队列。分为两种类型的任务:微任务(microtask)和宏任务(macrotask)
宏任务(macrotask):
- 包括:setTimeout、setInterval、setImmediate、I/O、UI renderingmacrotask事件;
- 可以理解为浏览器执行完当前宏任务后,在下一个宏任务执行之前,浏览器就会开始进行渲染;
- 宏任务一般是当前事件循环的最后一个任务,浏览器的ui绘制会插在每个宏任务之间,阻塞宏任务会导致浏览器ui不渲染;
- 其实也可以把主线程的任务当作第一个宏任务来看待。
微任务(microtask):
- 包括:Promises(浏览器实现的原生Promise)、MutationObserver、process.nextTick;
- 浏览器进行ui渲染之前执行的任务,也就是ui渲染是在微任务执行完成后才开始的;
- 值得注意的是,过多的微任务会阻塞浏览器的渲染;给microtask队列添加过多回调阻塞macrotask队列的任务;
- 鉴于上面问题,浏览器考虑性能的问题,也会对微任务的数量进行限制;
- 事件的冒泡行为,也是在微任务后执行,微任务的优先级是最高的; 举个例子:
console.log('main start');
setTimeout(() => {
console.log('macrotask');
Promise.resolve().then(() => {
console.log('microtask 1');
})
}, 0);
Promise.resolve().then(() => {
console.log('microtask 2');
Promise.resolve().then(() => {
console.log('microtask 3');
})
})
console.log('main end');
上面模仿了一下微任务(Promise)和宏任务(setTimeout);微任务里面套了个微任务;宏任务里面套了个微任务; 输出如下:
main start
main end
microtask 2
microtask 3
macrotask
microtask 1
可以分析下上面代码的执行顺序:
- 第一步:先执行的是主线程的代码main start和main end;
- 第二步:开始执行微任务microtask2,执行microtask2过程中,又添加了一个microtask3的微任务,
- 第三步:执行完microtask2后,继续从microtask队列中取微任务,发现有刚在执行microtask2过程中放进去的3,取出microtask3来执行microtask3;
- 第四步:执行完microtask3后,继续从microtask队列中去取微任务,此时微任务队列为空,则去宏任务队列中取任务,取到macrotask;
- 第五步:执行macrotask,执行的过程中,又往微任务队列存了个microtask1;
- 第六步:执行完macrotask后,此时一个宏任务执行完成,开始下一轮重复,也就回到了上面的步骤2,微任务队列获取微任务,发现了microtask1;
- 第七步:执行microtask1,执行完成,程序运行完成。
综上分析任务队列完成
nextTick
经过上面的过程,相信大家都对浏览器的运行机制和任务队列有了足够的了解,也明白了任务队列中任务的执行顺序,接下来咱们看下nextTick的实现。文件位于/src/core/util/next-tick.js,先看下Vue里面任务的代码:
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
console.log('counter', counter)
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
上面代码是Vue对timerFun的定义,Vue倾向于微任务,毕竟微任务优先级是最高的,咱们来看下实现:
- 最优先采用的是Promise,直接使用的也是咱们上面的例子:Promise.resolve().then(flushCallbacks);
- 如果浏览器不支持原生的Promise,退而求其次,使用浏览器自带的MutationObserver;MutationObserver,它会在指定的DOM发生变化时被调用;Vue的实现方式是创建一个dom节点,通过改变节点的内容,来触发MutationObserver的回调:new MutationObserver(flushCallbacks);
- 如果浏览器也不支持MutationObserver,那没办法了,只能使用宏任务了setImmediate和setTimeout,这两个在Vue里面使用方式是一样的,两者的执行顺序在无I/O的时候说不准,不过在有I/O的时候setImmediate是会先被执行的,这可能也是Vue先考虑使用setImmediate的原因吧。
来看下nextTick的代码吧:
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) {
console.log('_resolve')
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
上面部分代码,会对传进来的cb进行存储,放到全局变量callbacks里面,然后判断当前执行的状态,是否属于pending(类似Promise的pending状态)状态,可以理解为忙着呢,如果不忙,就让它忙起来,执行上面部分讲到的timerFun;这部分也就是咱们经常用到的
$nextTick(function() {
dosomething....
})
下面咱们来看下调用timerFun后,执行的flushCallbacks;
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
执行开始,置为不忙状态,因为浏览器是单线程的,执行这段代码的时候,就不会执行别的代码,不用担心这时候会有别的事情影响此处代码的执行,也不用担心此时会有nextTick的调用,也就不用担心pending状态的此处改变会不会影响nextTick部分的逻辑;
此处代码很简单,获取回调的拷贝,然后把回调栈清空;依次执行回调。
结语
nextTick本章讲完,篇幅较少,不过之所以把nextTick拿出来单独讲,主要是因为涉及到宏任务和微任务,对此处有疑惑的读者可以评论或私信。