深入剖析javascript的事件循环机制 Event loop

如何理解js事件循环是当前各大公司问的非常频繁的一个问题。很多人估计大概知道js事件循环是个什么东西,单可能在脑子里的概念又比较模糊,无法深入的理清楚。那么事件循环到底是个什么东西。今天我们来好好了解一下。不过在了解之前,我们需要先了解几个概念

1、js特性

首先我们需要知道javascript是一个单线程的语言。可js却又存在异步代码并且能够很好的被处理。那么它是如何很好的处理这些异步的呢?我们先保留这个疑问

2、队列

一个遵循,先进先出,后进后出的有序列表

3、执行栈

主线程(javascript引擎的线程)在运行过程中会产生一个东西叫做执行栈。而执行栈中,存在两个东西,一个叫堆,一个叫栈。堆里面存放着一些非结构化的数据或者是对象的内存空间。栈里面则是当前需要执行的代码。这些执行代码调用各种api(如:dom事件,ajax, setTimeout,Promise等)。并往任务队列中添加各种事件(可以理解为,执行过程中遇到任务源时,把任务源所对应的回调函数放入到队列中,至于什么是任务源,我们后面会说),待执行栈中代码执行完毕后,会依次取出任务队列中的数据放入到执行栈中继续执行

  • 1、主线程运行时,会产生执行栈(执行栈可以理解为代码运行的容器,如果把代码比作水,我们可以把执行栈比作是过水的通道),栈内代码执行过程中当遇到任务源时,将任务源中的回调函数加入到任务队列中
  • 2、执行栈中的代码执行完毕后,会依次调用任务队列中的代码放入到执行栈中执行,如果反复循环。就形成了我们所说的事件循环 Event loop
    在这里插入图片描述
4、同步和异步

了解完执行栈,我们就更好了解同步和异步了
首先,异步的出现就是为了解决javascript单线程的问题,单线程意味着所有的任务都需要排队等待,而异步恰恰很好的解决排队等待的问题

  • 在同一轮事件循环当中,同步任务会直接放入到主线程的执行栈进行执行,而异步任务不进行执行栈,而是进入任务队列中
  • 同步任务按顺序执行,执行完毕后,才会去读取队列中的可执行的异步任务,并将可执行的异步任务依次加入到执行栈中执行。如此反复循环
同步
异步
任务执行完成
不断读取
有回调时
任务进入执行栈
同步还是
主线程
挂起
任务队列

如此看来,我们上面所说的那些被加入到队列中的各种事件的回调函数,是不是就是我们所说的异步任务啦!

5、宏任务 和 微任务

每个事件循环中,都至少有一个宏任务队列和一个微任务队列,分别存放着不同任务源的任务

宏任务(macrotask)

可以简单的理解为每次执行栈中执行的代码

  • 宏任务在执行过程中可以创建宏任务,也可以创建微任务,创建的宏任务会加入到宏任务队列中,等待下一个循环执行。而创建的微任务会在本次宏任务中的代码全部执行完成以后立刻依次执行。故宏任务中创建的微任务不会影响当前宏任务的执行
  • 当一个宏任务执行完以后,会读取微任务队列中的任务,并依次全部执行完微任务队列中的任务。当微任务队列中的任务全部执行完成后,算是一个完整的task执行完成。
  • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 。如此过程不断重复,便是我们所受的事件循环,Event loop
  • 常见的宏任务: setTimeout, setInterval, requestAnimationFrame, I/O, script标签内代码,setImmediate (Node环境中) 等(这些也被称作任务源)
微任务(microtask)

可以简单的理解为当前宏任务执行完成后立刻被执行的任务

  • 微任务执行期在 本次宏任务执行完之后,下个宏任务执行之前,并且在UI渲染之前
  • 微任务中创建的宏任务,也会被加入宏任务队列中,等待依次执行
  • 微任务中创建的微任务,会在当前task中执行完成,也就是说微任务中创建的微任务,会在下一个宏任务执行前以及UI渲染前被执行
  • 常见的微任务有: Promise callback, process.nextTick(node环境中),MutationObserver (这些也被称作任务源)
    在这里插入图片描述
流程总结
  1. 主线程开始执行一段代码, 假设开始执行一个script标签内的代码,将代码放入执行栈中执行,同步代码优先执行,执行过程中当遇到任务源时,判断是宏任务还是微任务
  2. 如果是宏任务,加入到宏任务队列中,如果是微任务,加入到微任务队列中
  3. 同步代码执行完成,执行栈空闲,检查微任务队列中是否有可执行任务,如果有,依次执行所有微任务队列中的任务。如果没有。当前任务执行结束。
  4. 渲染UI
  5. 检查宏任务队列是否有可执行的宏任务,如果有,取出队列中最前面的那个宏任务,加入到执行栈中开始执行,然后重复 步骤1- 5。直到宏任务队列中所有任务执行结束
示例

下面我们来看几个例子
先看个简单的

<script>
console.log('start');
 
setTimeout(() => {
    console.log('timeout');
}, 0);

Promise.resolve().then(() => {
    console.log('promise');
});

console.log('end');
</script>

输出结果是:

start
end
promise
timeout
  • 主线程开始执行,script标签中的代码被加入到执行栈中执行,同步代码优先执行,依次输出 start => end
  • 执行过程中,碰到任务源 setTimeout ,将setTimeout的回调函数加入到宏任务队列中
  • 继续执行, 碰到任务源 promise 将 promise的回调函数加入到微任务队列中
  • end 输出后 执行栈空闲,检查微任务队列,发现有一个微任务,取出进入到执行栈中执行,输出promise
  • 微任务队列中所有微任务执行完成,开始下一个循环
  • 检查宏任务队列中是否有可执行的宏任务,发现有一个,取出放入执行栈中执行,输入timeout
  • 执行完毕
再看个复杂点的
<script>
 console.log('start');
 
 var timer1= setInterval(() => {
     console.log('timer1');
 }, 0);

var timer2= setInterval(() => {
     console.log('timer2');
 }, 0);
 
 setTimeout(() => {
     console.log('timeout1');
 }, 0);

 new Promise((resolve, reject) => {
     console.log('promise1');
     resolve()
     console.log('promise after promise1');
 }).then(() => {
     console.log('promise2');
     clearInterval(timer2);
 }).then(() => {
     console.log('promise3');
 });

 new Promise((resolve, reject) => {
     setTimeout(() => {
         console.log('setTimeout in promise');
         clearInterval(timer1)
         resolve();
     });

     console.log('promise after timeout');
 }).then(() => {
     console.log('promise4');
 }).then(() => {
     console.log('promise5');
 });

 Promise.resolve().then(() => {
     console.log('promise6');
 });

 console.log('end');
 </script>

正确的执行结果是:

start
promise1
promise after promise1
promise after timeout
end
promise2
promise6
promise3
timer1
timeout1
setTimeout in promise
promise4
promise5
  • 首先输出start,不多说,遇到setInterval事件源timer1,将回调加入到宏任务队列中
  • 继续执行,遇到setInterval事件源timer2, 将回调加入到宏任务队列中
  • 继续执行,遇到setTimeout事件源,将回调加入到宏任务队列中,此时宏任务队列中有3个任务,分别 timer1,timer2 和 setTimeout
  • 碰到第一个promise函数,promise执行函数内部是同步的,故直接输出promise1,继续执行,碰到resolve(), 返回一个回调,将这个回调(也就是第一个.then())加入到微任务队列,继续执行,输出promise after promise1
  • 碰到第二个promise,执行内部同步代码, 发现一个setTimeout,将其加入到宏任务队列中,此时,宏任务队列中存在 timer1, timer2 第一个setTimeout, 第二个setTimeout 一共4个任务
  • 继续执行,输出promise after timeout
  • 继续往下走,注意,后面那个.then(),此时我们并没有执行resolve(),故 不存在回调函数,故暂时挂起
  • 继续执行,碰到最后一个promise,存在resolve()回调,将回调加入到微任务队列,此时,微任务队列中,有两个任务
  • 继续执行,输入同步代码end
  • 同步代码执行完成,此时查看微任务队列中是否有可执行任务,此时,发现两个,依次取出执行,取出第一个执行,输出promise2,同时碰到clearInterval, 执行,并清除定时器timer2,
  • 此时,注意了,这个.then()执行完成后,返回的依旧是一个promise对象,故存在第二个.then()的回调,将此回调加入到微任务队列中。此时,微任务队列中又新增了一个任务
  • 从宏任务队列中移除timer2 ,宏任务队列中剩余 timer1 和 两个 setTimeout
  • 继续执行下一个微任务,输出promise6
  • 继续,发现队列中还有一个微任务,是刚刚我们执行第一个微任务时产生的微任务,执行此微任务,输出promise3
  • 此时,微任务队列为空,渲染UI,开始下一个循环
  • 检查宏任务队列,取出第一个宏任务加入执行栈执行,输出timer1,同步执行完成,检查微任务队列,发现为空,没有微任务,继续进行下一轮循环
  • 取出下一个宏任务(此时,timer2之前已经被我们清除了,故不执行),是个setTimeout,输出timeout1,同步代码执行完毕,检查微任务队列,无,继续下一个循环
  • 取出下一个宏任务,是第二个setTimeout, 执行,输出setTimeout in promise,继续执行,清除timer1
  • 继续执行,执行了resolve()回调,将.then()回调加入到微任务队列中, 同步代码执行完毕。此时检查微任务队列,发现存在一个我们刚刚加入的微任务,取出执行,输出promise4
  • 此时,注意,该.then()回调执行完后,返回的又也一个promise回调,故将第二个.then()加入到微任务队列中。
  • 继续检查微任务队列,取出刚加入的微任务并执行,输出promise5。
  • 开始下一个循环,发现宏任务队列为空。
  • 代码执行结束


看完这些,是否对js的事件循环已经了然于胸了呢。如果还没明白,请多看几遍。也可直接留言询问。感谢阅览

  • 28
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值