一道javascript面试题引发了自己对eventloop的探索 先贴入这道题:
console.log('main1');
process.nextTick(function() {
console.log('process.nextTick1');
});
setTimeout(function() {
console.log('setTimeout');
process.nextTick(function() {
console.log('process.nextTick2');
});
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then');
});
console.log('main2');
在分析面试题之前我们来说一下JS的事件环,我们都知道JS是单线程的,也就是说任务需要一个接一个的按顺序执行,这是因为JS作为浏览器端的脚本语言其开始主要用途还是与用户互动和操作DOM,假如JS有两个线程,一个添加DOM一个删除DOM,这就势必会出现不可预期的后果,所以说还是单线程更适合,但是这种方式有一个弊端,就是必须要等待前一个程序执行完毕才执行下一个,所以将程序分为了两类:同步任务和异步任务。 在JS的执行栈中,同步任务进入主执行栈(也可以说主线程),而异步任务进入任务队列(TaskQueue)等待执行,任务队列可以理解成一个消息队列,I/O设备完成一件事,就向任务队列添加一个事件,一旦主执行栈中所有的同步任务执行完毕,就会读取任务队列中等待的任务,并放入执行栈开始执行,其实就是执行异步任务的回调函数,所以说异步任务必须指定回调函数,主线程会不断的循环这个动作,所以这种运行机制又称为EventLoop(事件循环)。( 这里解释一下什么叫‘任务队列’:这是一个先进先出的数据结构,排在前面的事件会优先被主线程读取,但是当有定时器的时候主线程会先检查一下执行时间。)
我们画一张图来理解这个:
从这张图中我们可以看到其中有宏任务(MacroTask)和微任务(MicroTask)之分,我们来说下这个宏任务与微任务。 宏任务包括: *setImmediate *setTimeout *setInterval 微任务包括: *process.nextTick *Promise *MutaionObserver *Object.observe(已废弃:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) 在单次的迭代中,event loop首先检查Macrotask队列,如果有一个Macrotask等待执行,那么执行该任务。当该任务执行完毕后(或者Macrotask队列为空),event loop继续执行Microtask队列。(V8 中 Microtask 默认是自动运行的)。 在这里再说下堆(heap)和栈(stack),一般会划分出两种不同的内存空间堆和栈。先说一下栈吧,这是一种先进先出的数据结构,stack是有结构的,每个区块按照一定的次序存放而且可以知道其区块大小,与之配套的还有push,pop,top等方法。而一般不确定大小的数据都会放在heap(堆)里。(关于heap和stack欢迎补充)
看完上面的想必对JS的事件循环也有了一个基本了解,下面来开始分析这道题:
JS代码开始从上自下单线程执行:
1.console.log('main1');进入执行栈执行
2.遇到process.nextTick将它的回调函数先放入MicroTask(微任务)
3.遇到setTimeout将它的回调函数放入MacroTask(宏任务队列)
4.在执行栈中new Promise 并将.then中注册的回调放入MicroTask
5.最后一行代码console.log('main2');会放入主执行栈执行
综上所述,这段代码的结果就是先输出main1,然后第二步第三步我们不用管它,它不是在主执行栈中,
所以直接到第四步输出promise,然后主执行栈继续执行第五步输出main2。此时主执行栈执行完毕,开始
事件循环,发现在MicroTask中还有任务,开始清空微任务,第二步中我们在微任务中放入了
process.nextTick所以输出process.nextTick1,在第四步中将.then的回调放入了微任务,那么微任务
队列继续执行输出promise then,此时微任务队列已经清空开始事件循环宏任务队列,也就是输出第三步
中的setTimeout,在输出之后发现setTimeout这个回调中还有一个process.nextTick,那么这个回调继
续放入微任务队列,此时事件循环发现主执行栈中已经没有任务,那么开始执行MicroTask输出:
process.nextTick2所以这道题的答案是:
//main1,promise,main2,process.nextTick1,promise then,setTimeout,process.nextTick2
在这道题中在主进程执行完毕后,应该会调用任务队列的宏任务,但是在这之前要清空了MicorTask微任务,
这与我们先说的一次事件循环的循环机制(先读取宏任务后读取微任务)相悖,所以说主进程的代码也
是一个MacroTask(参考:Promises/A+规范),也就是说执行完主进程的代码后执行MicroTask这是第一个事件
循环(event loop),然后又开始执行任务队列(MacroTask)和process.nextTick2属于第二个事件循环。
下图为此题分析图: