前言
最近看到一些关于 事件队列,浏览器执行机制的文章推荐,联想到很早以前遇到的一些面试题,才惊觉自己对这块一直都不怎么了解,借助这个机会好好记录一番。顺便感叹一句,阮一峰大神的 blog真是应有尽有,好好搜索一番就能找到想要的文章。还有每周前言科技推送,非常值得关注,怪不得能被封神。
一道题目
setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');
这道题的运行结果是:
C
E
D
A
B
在讲解这道题目之前,先说下这道题涉及到一些的概念:(此题讲解在最后)
- 同步任务与异步任务
- 异步任务类型
- 事件循环
- macroTask与microTask
同步任务 VS 异步任务
这两个词相信所有的前端工程师都听过,并且最早接触前端的时候也没分过同步和异步。直到开始接触ajax请求,了解了异步这回事情,才直到与之相对的就是同步。 但到底什么是同步,什么是异步呢? 这里说下我的理解方式,线性执行下去的任务就是同步任务。而需要等待一定时间延后执行的就是异步任务。(这只是便于理解的简单解释)
官方解释是:
- 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
- 异步任务:不进入主线程,而进入“任务列队”的任务,只有等主线程任务执行完毕,“任务队列”开始通知主线程,请求执行任务,该任务才会进入主线程执行。
这里有需要注意的是:同步任务与异步任务在一起时,主线程总是要等同步任务执行完成后,才有空闲去执行异步任务的“任务队列”中的任务。( js的单线程的体现 )
一个🌰:
console.log('同步任务开始');
// 异步任务
setTimeout(() => {
console.log('异步任务开始');
}, 0);
for(var i = 0; i < 10; i++ ) {
console.log(i);
}
在这个例子中,异步任务就是setTimeout,当浏览器解释到这一句的时候,发现其是异步任务,将其回调函数放入task queue,等待执行。所以会先打印最后的0 ~9 ,当执行完同步任务后,主线程开始空闲,于是去执行任务队列中的task。
异步任务类型
Javascript是单线程运行,异步操作特别重要。为了协调异步任务,Node提供了四个定时器,让任务可以在指定的时间运行。
- setTimeout (clearTimeout)
- setInterval (clearInterval )
- setImmediate (clearImmediate)
- process.nextTick
- ....
前两个定时器都是比较常用的,setTimeout是在指定时间段以后执行回调,setInterval是在指每间隔一段时间,就去执行一个任务。
process.nextTick可以在当前“执行栈”的尾部,下一次Event Loop之前触发回调函数。即,它指定的任务总是在所有异步任务之前。
setImmediate也是一个定时任务,它总是在“任务队列”的尾部添加事件,即它指定的任务总是在下一次Event Loop执行时。
事件循环(event loop)
每一个“线程”都有一个独立的event loop, 每一个web worker也有一个独立的event loop, 所以它可以独立的运行。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
Macrotask VS MicroTask
microTask指的就是事件循环中的“任务队列”中的task。
macroTask在有些文章中称为Task,这里为了和microTask形成一致性的对应,采用macroTask一词。
microtask通常来说就是在当前主线程任务执行结束后立即执行的任务,比如需要对一系列的任务作出回应,或者是需要异步的执行任务而又不需要分配一个新的task, 这样可以减小一点性能开销。
microtask与macrotask任务队列是相互独立的队列。
每一个macrotask中产生的microtask都将会添加到microtask队列中。 microtask中产生的microtask将会添加至当前队列的尾部,并且 microtask 会按序的处理完队列中的所有任务。
microtask 包括:process.nextTick , promise, Object.observer, MutationObserver
macrotask 包括(就是tasks):setTimeout, setInterval, setImmediate, I/O, UI 渲染
microtask 和 macrotask并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:
1、event-loop start
2、microTasks 队列开始清空(执行)
3、检查 macrotask(Tasks) 是否清空,有则跳到 4,无则跳到 6
4、从 macrotask(Tasks) 队列抽取一个任务,执行
5、检查 microTasks 是否清空,若有则跳到 2,无则跳到 3
6、结束 event-loop
一个🌰
console.log('script start');
// macroTask
setTimeout(function() {
console.log('setTimeout');
// macroTask中的microTask会添加至 microTask队列中,等待下次执行
process.nextTick(() => {
console.log('nextTick1');
});
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
// microTask中的microTask会添加至 当前microTask队列的尾部,在下一个macroTask执行之前执行
process.nextTick(() => {
console.log('nextTick2');
});
}).then(function() {
console.log('promise2');
});
结果是:
script start
script end
promise1
promise2
nextTick2
setTimeout
nextTick1
题目解析
如果上面的例子都能够理解的话,相信第一题的答案就能够脱口而出了,现在可以回到第一题,再细细品以下😁 如果还是有困难的话,请接着往下看:
// macroTask
setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
// microtask
return new Promise(function (resolve) {
// 同步任务
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
// 同步任务
console.log('E');
由于同步任务优先于异步任务执行,所以C在obj.func()执行的时候输出,然后是E。 其实解释器在先执行第一段的setTimeout时,发现其是一个异步任务,故将其放入macrotask队列中,在执行obj.func()时,又有一个setTimeout,同样的将其放入 macrotask队列中,根据先进先出原则,先输出的是A,然后是B。对于promise函数来说,它是一个microTask,所以先于macrotask执行,故先输出D。
所以顺序就是: C,E,D,A,B
后记
在学习事件队列,microtask, macrotask的时候,查阅了很多技术文章,发现了阮一峰大神文章下面的评论,也发现了朴灵老师的犀利批评。
这里我不发表对阮老师和朴灵老师的评价,只是告诫下大家,技术日新月异,具有实效性,希望大家在查阅文章的时候,能够积极的实践下。
阮老师的文章更多的是科普性质的,并且他非常高产,也经常性的查看外文资料。不过技术扎实性可能会有些偏差,不过这不影响他的封神,本来对于技术的理解每个人都是不一样的。
朴灵老师的《深入浅出nodeJS》一书中也讲述了事件循环,不过更多的是用精简的语言描述,当然每句话中都有很多的深奥的专业术语。比如“红黑树”,“观察者”等等,如果真正要吃透,就需要花非常多的时间。
大家各取所需吧,并且标准是标准,在浏览器厂商实现方面可能又会有一些偏差。所以个人推荐还是“实践出真知吧”!😄