
这张图很好的解释了js处理并发的问题,其实主线程就是执行栈里的方法,一个一个的执行,出现异步就放在堆里,然后堆里的异步执行完成之后,就放到事件队列里。等主线程空了之后,js就会去执行事件队列里的回调函数。
而js主线程执行完去执行事件队列,然后再执行主线程的循环过程,叫做事件循环(Event Loop)
具体是怎么做的呢?
主线程先执行同步的任务,执行过程中遇到异步任务,就扔到堆(event table)里去执行主线程会继续执行同步的任务,event table里的异步任务在执行完后,按顺序在任务队列(event queue)里注册回调函数。同步任务执行完之后(即栈空了之后),js引擎的monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。有就会去取任务队列里的回调函数按顺序执行,执行回调函数的过程中,遇到异步继续放到堆里执行,主线程执行完任务队列里的回调函数之后,继续执行栈里的方法,以此循环。
宏任务(Task)和微任务(microTask)
这两个任务进入的queue是不同的。宏任务进宏任务队列,微任务进微任务队列。
在js主线程在解析文件的时候,会先把执行这个文件设置成为一个宏任务,然后放到宏任务队列,然后一行一行代码的往下执行。

在主线程第一次执行的时候,就是在执行宏任务队列的第一个任务,就是执行当前代码。宏任务结束的时候,去查看微任务队列里是否有可以调用的回调函数。如果有,就执行所有的微任务的回调函数,然后开启新的宏任务。没有的话,就会直接开始新的宏任务。


nodeJs里的事件循环
就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate。
setImmediate与setTimeout的区别
在官方文档中的定义,setImmediate
为一次Event Loop
执行完毕后调用。setTimeout
则是通过计算一个延迟时间后进行执行。
但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。
因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs
,而这时定时器已经处于可执行回调的状态了。
所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop
,这时才会执行setImmediate
。
- setTimeout 先执行的方法,在两个方法的后面加一个循环,时间超过setTimeout的时间,则setTimout的回调已经可以调用了,而setImmediate的会等循环结束之后再注册回调函数。
- 在一个宏任务里,setImmediate一定先执行
process.nextTick
可以认为是一个类似于Promise
和MutationObserver
的微任务实现,在代码执行的过程中可以随时插入nextTick
,并且会保证在下一个宏任务开始之前所执行。
// eg1:
console.log(1);
setTimeout(() => {
console.log(2);
});
process.nextTick(()=>{
console.log(3);
});
setImmediate(()=>{
console.log(4);
});
new Promise(resolve=>{
console.log(5);
resolve();
console.log(6);
}).then(()=>{
console.log(7);
})
Promise.resolve().then(()=>{
console.log(8);
process.nextTick(()=>{
console.log(9)
})
})
例1就比较好了解了:JS栈在运行的时候,先把整个代码run script放到宏任务队列里。
- 打印1,
- 然后开始往下执行遇到setTimeout,就扔到task table里,因为没有延时,回调函数就直接进入task queue里。
- 继续往下,遇到process.nextTick,扔到microTask queue里,
- 遇到setImmediate,回调扔到task queue里。
- 继续往下,执行new Promise,打印5,6。
- 往下执行then,把then回调函数扔到microTask queue里,
- 把then 的回调函数扔到microTask queue里。
第一个宏任务执行完了,发现microTask queue里有好多任务,就调用里面的方法
- 执行第一个process,打印3
- 执行第一个then,打印7
- 执行第二个then,打印8,吧process扔到microTask queue里
- 执行第二个process 打印9
第一个宏任务执行完,且没有微任务了,执行第二个宏任务
- 执行setTimeout的回调函数,打印2
第二个宏任务执行完,没有微任务了,执行第三个宏任务
- 执行setImmediate的回调函数,打印4
所以答案就是: 1,5,6,3,7,8,9,2,4
// eg2:
console.log('start');
const interval = setInterval(() => {
console.log('setInterval');
}, 0);
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve()
.then(() => {
console.log('promise 3');
})
.then(() => {
console.log('promise 4');
})
.then(() => {
setTimeout(() => {
console.log('setTimeout 2');
Promise.resolve()
.then(() => {
console.log('promise 5');
})
.then(() => {
console.log('promise 6');
})
.then(() => {
clearInterval(interval);
});
}, 0);
});
}, 0);
Promise.resolve()
.then(() => {
console.log('promise 1');
})
.then(() => {
console.log('promise 2');
});
这道题可以这么看:
js栈第一次执行,把 run script 放到task queue里,然后开始执行代码
- 打印 start
- 把setInterval的回调函数扔到task queue里
- 把setTimeout 的回调函数扔到task queue里
- 把两个then扔到microtask queue里
第一个宏任务run script 执行完毕,microtask queue里不为空
- 执行第一个then,打印promise 1
- 执行第二个then,打印promise 2
第一个宏任务执行完毕,且microtask queue为空,执行第二个宏任务
- 打印setInterval,并把其放到task queue里
第二个宏任务执行完毕,且microtask queue为空,执行第三个宏任务
- 打印setTimeout1
- 把三个then扔到microtask queue里
第三个宏任务执行完,但microtask queue不为空
- 执行第一个then 打印 promise 3
- 执行第二个then 打印 promise 4
- 执行第三个then,遇到setTimeout ,扔到任务队列里
第三个宏任务执行完毕,microtask queue为空,执行下一个宏任务
- 打印setInterval, 并把其放到task queue里
第四个宏任务执行完毕,且microtask queue为空,执行下一个宏任务
- 打印setTimeout2
- 把三个then扔到microtask queue里
第五个宏任务执行完,micortask queue不为空,执行微任务
- 打印promise 5
- 打印promise 6
- 终止掉task queue里的setInterval的回调函数。
所以答案就是:
start promise 1 promise 2 setInterval setTimeout 1 promise 3 promise 4 setInterval setTimeout 2 promise 5 promise 6