一、循环机制(event loop)之宏任务和微任务
js任务分为同步任务和异步任务,异步任务又分为宏任务和微任务,其中异步任务属于耗时的任务
console.log(1)
setTimeout(function(){
console.log(2);
let promise = new Promise(function(resolve, reject) {
console.log(7);
resolve()
}).then(function(){
console.log(8)
});
},1000);
setTimeout(function(){
console.log(10);
let promise = new Promise(function(resolve, reject) {
console.log(11);
resolve()
}).then(function(){
console.log(12)
});
},0);
let promise = new Promise(function(resolve, reject) {
console.log(3);
resolve()
}).then(function(){
console.log(4)
}).then(function(){
console.log(9)
});
console.log(5)
二、宏任务和微任务有哪些?
1. 宏任务: 整体代码script、setTimeout、setInterval、setImmediate(Node.js)、i/o操作(输入输出,比如读取文件操作、网络请求)、ui render(dom渲染,即更改代码重新渲染dom的过程)、异步ajax等
2. 微任务: Promise(then、catch、finally)、async/await、process.nextTick(Node.js)、Object.observe(⽤来实时监测js中对象的变化,已废弃)、 MutationObserver(监听DOM树的变化)
三、nextTick正文开始
官方给的解释就是:等待下一次 DOM 更新刷新的工具方法。
nextTick我们经常在项目中用到,简单的理解他就是一个setTimeout函数,将函数放到异步后去处理;将它换成setTImeout也能跑起来,但它仅仅就这么简单吗?那我们为什么不直接用setTimeout呢?
四、nextTick作用
- 确保DOM更新完成;
- 避免不必要的DOM操作;
- 优化性能。
这里要提示一下:DOM更新和UI渲染是两个东西
在给data中的数据赋值后立马去查看数据导致的。由于 “查看数据” 这个动作是同步操作的,而且都是在赋值之后;因此我们猜测一下,给数据赋值操作是一个异步操作,并没有马上执行,Vue官网对数据操作是这么描述的:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
五、nextTick源码分析
(源码位置:/src/core/util/next-tick.js)
了解了nextTick的用法和原理之后,我们就来看一下Vue是怎么来实现的。
const callbacks = []
let pending = false
let timerFunc
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)
}
})
// 如果当前没有在 pending 的回调,就执行 timeFunc 函数选择当前环境优先支持的异步方法
if (!pending) {
pending = true
timerFunc()
}
// 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
我们首先找到nextTick这个函数定义的地方,看看它具体做了什么操作;看到它在外层定义了三个变量,有一个变量看名字就很熟悉:callbacks,就是我们上面说的队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。
callbacks新增回调函数后又执行了timerFunc函数,pending用来标识同一个时间只能执行一次。那么这个timerFunc函数是做什么用的呢,我们继续来看代码:
// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeOut 最小延迟也要4ms,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持
// 多次调用 nextTick 时 ,timerFunc 只会执行一次
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持Promise
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 如果不支持 promise,就判断是否支持 MutationObserver
// 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//判断2:是否原生支持MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
// 监听这个文本节点,当数据发生变化就执行 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)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
这里出现了好几个isNative
函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise
,虽然我们使用了垫片(polify)
,但是isNative(Promise)
还是会返回false
。
可以看出这边代码其实是做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then、MutationObserver
和setImmediate
,上述三个都不支持最后使用setTimeout
;降级处理的目的都是将flushCallbacks
函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。MutationObserver
是Html5
的一个新特性,用来监听目标DOM
结构是否改变,也就是代码中新建的textNode
;如果改变了就执行MutationObserver
构造函数中的回调函数,不过是它是在微任务中执行的。
那么最终我们顺藤摸瓜找到了最终的大boss:flushCallbacks
;nextTick
不顾一切的要把它放入微任务或者宏任务中去执行,它究竟是何方神圣呢?让我们来一睹它的真容:
/** 作用:用来执行callbacks中的回调函数 */
/** 为什么要拷贝callbacks?因为nextTick回调中还有可能会调用nextTick,就有可能向callbacks中添加回调,
就有可能一直循环。nextTick回调中的nextTick应该放在下次的循环里面 */
/** 为什么使用slice? 因为slice修改原数组或者复制后的数组不会互相影响 */
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
六、总结
到这里,整体nextTick
的代码都分析完毕了,总结一下它的流程就是:
- 把回调函数放入callbacks等待执行
- 将执行函数放到微任务或者宏任务中
- 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调
nextTick
中维护了一个callbacks
队列,一个pending
锁,一个timerFunc
。
这个timerFunc
就是根据浏览器环境判断得出的一个能够产生微任务或降级为宏任务的api调用,比如promise.then
。
callbacks
队列的作用是收集当前正在执行的宏任务中所有的nextTick
回调,等当前宏任务执行完之后好一次性for
循环啪执行完。
试想如果没有callback
队列的话,每次调用nextTick
都去创建一个timerFunc微任务(假设支持),那么也就不需要pending
锁了。
现在有了callbacks
队列的情况下就只需要创建一个timerFunc
微任务,那问题是什么时候创建该微任务呢?
这里就要讲到pending
了,在pending
为false
的时候表示第一次添加cb
到callbacks
中,这时候创建一个timerFunc
微任务,并加锁。
后面调用nextTick
就只是往callbacks
添加回调。
等当前宏任务之后完之后,就会去执行timerFunc
清空callbacks
队列,并设置pending
为false
,一切归零
七、其他
答案:打印顺序 1、3、 5、 4、 9、 10、 11、 12、 2、 7、 8