关于事件循环的定义参考这篇文章:事件循环机制EventLoop
大家准备好了吗?我要开始复制粘贴了
不不不,我要开始整理精华了(毕竟,我觉得博客都有遗漏之处,可能作者理解了但是忘了写)
强迫症的我告诉自己必须再详细解释一番
关于定义,大家可以反复阅读上面的一片文章
MacroTask(宏任务):
script全部代码、
setTimeout、
setInterval、
setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、
I/O、
UI Rendering
MicroTask(微任务):
Process.nextTick(Node独有)、
Promise的callback回调(注意Promise的函数直接执行,算script里面的代码)、
Object.observe(废弃)、
MutationObserver(具体使用方式查看这里)
偷图一张如下:
例子开始:
console.log(1); // script代码
setTimeout(function () { // 异步模块,执行完放到专有的宏任务队列
console.log(2);
});
new Promise(function(resolve,reject){ // Promise里的的函数直接执行,算是script代码
console.log(3);
resolve(4); // 只有执行resolve或者reject才会有回调,不执行这个就不会执行then里面的回调函数
}).then(function(val){
console.log(val); // 微任务
})
console.log(5); // script代码
执行结果:
过程讲解:
1.首先执行主线程这个宏任务,从上到下执行,遇到console.log(1); (script代码,打印1)
2.遇到setTimeout,把它丢给异步模块定时器处理,然后继续往下执行,并不会阻塞定时器,而此处定时器线程会在,主线程执行完后,把回调函数放入宏任务队列,注意宏任务队列可以有很多个,但是微任务队列只有一个。
3.遇到new Promise,直接执行,console.log(3)(里面算script代码,打印3)
4.执行resolve方法,最终会回调then里面的函数, 属于微任务,则把回调函数放入微任务队列
5.遇到console.log(5) 打印 ‘5’ 出来
6.一个宏任务执行完后(这里就是执行了全部script代码,算一个宏任务),会执行所有待执行的微任务队列里面的所有任务,所以打印 4 。
7.检查是否需要渲染UI,这是宏任务,也是上面那张图的示意要求,一轮事件循环结束了
第一轮循环里的宏任务和微任务都会被移除出任务队列,接下来开启第二轮循环
8.首先查找是否有宏任务,由于setTimeout 的回调被放入了宏任务队列,这里会执行回调函数的代码,打印了 ‘2’ 出来
9.接着查找是否有微任务,发现没有微任务,检查是否需要渲染UI,然后本轮循环结束
接下来会重复上面的步骤,这就是event loop 了。后续当我们触发点击事件,有回调函数的话,回调函数也会被放入宏任务队列,一旦队列里重新有了任务,就会被执行。
关于promise的resolve和then的问题
promise是立即执行的,它创建的时候就会执行,不存在将promise推入微任务中的说法;
resolve()是用来表示promise的状态为fullfilled,相当于只是定义了一个有状态的Promise,但是并没有调用它;
promise调用then的前提是promise的状态为fullfilled;
只有promise调用then的时候,then里面的函数才会被推入微任务中;
大家是否理解了呢?我们来看一个复杂一点的例子,把上面的知识串起来了
(答应我,上面没看懂就别往下看了,否则就像下面这张图)
console.log('start'); // script代码
var intervalA = setInterval(() => { // 放到异步处理模块
console.log('intervalA');
}, 0);
setTimeout(() => { // 放到异步处理模块
console.log('timeout');
clearInterval(intervalA);
}, 0);
var intervalB = setInterval(() => { // 放到异步处理模块
console.log('intervalB');
}, 0);
var intervalC = setInterval(() => { // 放到异步处理模块
console.log('intervalC');
}, 0);
new Promise((resolve, reject) => { // 直接执行
console.log('promise');
for (var i = 0; i < 10000; ++i) {
i === 9999 && resolve(); // 满足条件执行一次
}
console.log('promise after for-loop');
}).then(() => { // 上面i为9999的时候执行了resolve,所以会将这里加入微任务队列
console.log('promise1');
}).then(() => {
console.log('promise2');
clearInterval(intervalB);
});
new Promise((resolve, reject) => { // 直接执行
setTimeout(() => { // 放到异步处理模块,在这之前resolve没执行,后面then回调函数不会放入微任务
console.log('promise in timeout');
resolve();
});
console.log('promise after timeout');
}).then(() => {
console.log('promise4');
}).then(() => {
console.log('promise5');
clearInterval(intervalC);
});
Promise.resolve().then(() => { // 放入微任务
console.log('promise3');
});
console.log('end');
运行结果:
解析:
第一轮,执行全部属于script的代码,打印
start
promise
promise after for-loop
promise after timeout
end
到这里,宏任务执行了一个(全部script代码),接下来执行目前微任务队列的所有任务。
目前的微任务队列如下:
promise1 --->队头
promise3 --->队尾
外部的第二个 then 的注册,需要等待外部的第一个 then 的同步代码执行完成。所以这里没有promise2
执行了第一个then之后,打印promise1,返回Promise对象,队头出队,再次注册第二个then,此时微任务队列任务如下:
promise3 ---->队头
promise2 ---->队尾,执行promise2的时候clearInterval(intervalB);清除了B定时器
然后依次执行完微任务,检查是否需要渲染。
接下来看第二个宏任务队列,这里全部是异步模块定时器处理后放入的任务
执行宏任务,打印intervalA,检查微任务队列为空,检查是否渲染
执行宏任务,打印timeout,检查微任务队列为空,检查是否渲染
执行宏任务,打印intervalC,检查微任务队列为空,检查是否渲染(intervalB刚刚被取消了所以不执行)
执行宏任务,打印promise in timeout,接着执行resolve方法,表示promise的状态为fullfilled,promise调用then的前提是promise的状态为fullfilled,所以此时then里面的函数推入微任务队列。宏任务执行完毕,
执行微任务打印promise4,注册第二个then,进入微任务队列,再执行微任务promise5,任务执行完毕出队,然后微任务队列空,检查是否需要渲染。
接着最后检查宏任务队列为空,检查微任务队列为空,是否需要渲染,取消事件订阅,该事件的循环结束。
看到这里,相信大家对于事件循环EventLoop有了更深刻的理解,接下来,请大家看到文章开头推荐的文章再读几遍即可。
参考文章:
https://blog.csdn.net/tzllxya/article/details/92674042
https://blog.csdn.net/qq_39539687/article/details/100557701
===============Talk is cheap, show me the code================