【浏览器事件循环】

前言

本文主要讲解事件循环这个知识点,从自己学习理解出发,详细陈述一下对浏览器事件循环的学习和理解,并且会从易到难来举若干道实际的例子。

首先我们需要了解:

1:Javascript是单线程语言。
2:事件循环(Event Loop)是Javascript的执行机制。

浏览器事件循环:

将任务分为同步任务和异步任务。Js执行程序代码时,同步任务会进入主线程执行,而异步任务则会进入任务队列(Event Queue)中,当主线程中执行完毕,任务为空时,任务队列中的任务便会逐个进入主线程。

(那么在这里有个思考,当主线程中的任务为空时,任务队列中的任务以何种排序方式进入主线程,这个思考其实便是浏览器事件循环的关键所在,难点所在)

图片:

image.png

代码:

//A
console.log('1');                              

//B
setTimeout(function() {
  console.log('2');
}, 0);

//C
Promise.resolve().then(function() {
  console.log('3');
}).then(function() {
  console.log('4');
});

//D
console.log('5');
答案:  1  5  3  4  2

答案分析: (用字母来表示代码执行顺序),
因为A是同步任务,所以A进入主线程进行执行,输出 1。 因为B是异步任务,所以B进入(宏任务)任务队列。同样C是异步任务,所以C进入(微任务)任务队列。 D是同步任务,所以D进入主线程执行,输出5。由此我们可知:1 5 已经输出,那么 3 4 2 是怎么输出的楠,其实这就是我上述所引出的思考性问题,也是我们下面重点将要讲述的内容。讲下文讲述宏任务与微任务,宏任务队列与微任务队列。

异步任务分类:

异步任务分为宏任务(macro)和微任务(micro),所以浏览器事件循环中的异步队列也有两种:macro(宏任务)队列和 micro(微任务)队列。

常见的宏任务 (macro-task)比如:setTimeout、setInterval、script(整体代码)等。

常见的微任务(micro-task) 比如: new Promise().then(回调)、process.nextTick、MutationObserver(html5新特性) 等。

对于script(整体代码)也是宏任务,这个可能会给我们的理解上带来困惑,上述讲任务分为同步任务和异步任务,然后异步任务在分为宏任务和微任务,可是现在又讲宏任务中包含 script(整体代码),这个会造成误解,对于这种情况,我们可以理解为 script(整体代码)为宏任务,那么代码第一次执行的其实是script(整体代码)这个宏任务,在执行script时,若遇到宏任务,则将其加入宏任务队列,遇到微任务,则将其加入到微任务队列,遇到同步任务,则在主线程执行,等到同步任务执行完毕后,再将script(整体代码)宏任务在这一轮所产生的微任务依次进入(根据队列的先进先出原则)主线程进行执行,(在每个微任务执行过程中,可能同样会产生宏任务,微任务,同步任务,主线程先执行同步任务,然后主线程执行微任务,而宏任务被放入宏任务队列), 当这一轮的微任务在主线程上全部执行完毕后,主线程会去执行宏任务,当然,执行宏任务过程中,依旧可能会产生宏任务,微任务,同步任务,主线程同样先执行同步任务,然后主线程在执行微任务,将宏任务放入宏任务队列中。

先执行宏任务再执行当前宏任务内的微任务(在此过程中将产生的宏任务依次推入到宏任务队列,微任务进入到微任务队列),执行完当前宏任务内的所有微任务之后,再执行下一个宏任务,以此循环,当然在微宏任务中的同步任务会最先执行,而整体的scrpt标签就是第一个执行的宏任务。

上述俩段话就是一个总结,下面我们用步骤的方式在概括一下,大家只需理解记忆就行:

1.先执行script(整体代码)这个宏任务,在这个过程中会产生同步任务,微任务,宏任务,主线程先执行同步任务,微任务放入微任务队列中,宏任务放入宏任务队列中。

2.然后将script(整体代码)这个宏任务在执行过程中所产生的所有微任务依次从微任务队列中进入主线程执行。每一个微任务在执行过程中,可能会产生同步任务,微任务,宏任务,那么主线程先执行同步任务,在执行微任务,最后将宏任务进入到宏任务队列中。当微任务全部执行完后,则主线程开始执行宏任务。

3.执行宏任务时。也可能会产生同步任务,微任务,宏任务,同样主线程先执行同步任务,然后是微任务,而宏任务则进入宏任务队列。当微任务执行完后,宏任务队列中的一个宏任务会进入到主线程,主线程继续执行该宏任务,也可能会产生同步任务,…以上循环,直到程序结束。

什么是一个 事件循环Event Loop

主线程执行一个宏任务,并且主线程执行完这个宏任务所产生的同步任务和微任务,并将所产生的宏任务依次加入到宏任务队列中 的这个过程。

然后继续执行下一个宏任务…。不断的这些过程形成了浏览器事件循环。

图片:

image.png

例题

下面便是若干例题,可以好好看看:

例题1:

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

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

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

答案:main promise setTimeout
答案分析:先执行整体Script(整体代码)这个宏任务,这个宏任务会产生 setTimeout 这个宏任务,(将setTimeout加入宏任务队列),会产生 Promise.resolve().then 这个微任务,(将Promise.resolve().then 加入微任务队列),会产生 console.log(‘main’) 这个同步任务,所以主线程执行 console.log(‘main’) 并输出 main ,同步任务执行完毕后,便开始执行微任务,所以 Promise.resolve().then 进入主线程执行,Promise.resolve().then 在主线程中执行时,会产生 console.log(‘promise’) 这个同步任务,所以 主线程执行 console.log(‘promise’),输出 promise,等Promise.resolve().then 这个微任务执行结束后,(就是一个 事件循环 的过程),然后开始执行新的宏任务,setTimeout 这个宏任务会进入主线程执行,会产生 console.log(‘setTimeout’) 这个同步任务, 所以主线程执行 console.log(‘setTimeout’),并输出setTimeout ,然后 setTimeout这个宏任务执行完毕。

例题2:

<script>
        setTimeout(() => {                          //A
            Promise.resolve().then(() => {          //A
                console.log('promise');
            });
        }, 0);

        Promise.resolve().then(() => {             //B
            setTimeout(() => {                    //B
                console.log('setTimeout');
            }, 0);
        });

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

答案:main promise setTimeout

用于区分 加了 //A //B

答案分析:先执行整体Script(整体代码)这个宏任务,这个宏任务会产生 setTimeout //A 这个宏任务,(将setTimeout //A加入宏任务队列),会产生 Promise.resolve().then //B这个微任务,(将Promise.resolve().then //B 加入微任务队列),会产生 console.log(‘main’) 这个同步任务,所以主线程执行 console.log(‘main’) 并输出 main ,同步任务执行完毕后,便开始执行微任务,所以 Promise.resolve().then 进入主线程执行,Promise.resolve().then 在执行时,会产生 setTimeout //B 这个宏任务,所以 将 setTimeout //B 同样加入到宏任务队列 ,等Promise.resolve().then //B这个微任务执行结束后,(就是一个 事件循环 的过程)

然后开始执行新的宏任务,setTimeout //A这个宏任务会进入 (为什么是 setTimeout //A 先进入主线程,而不是setTimeout //B,因为队列的先进先出原则) 主线程执行,会产生 Promise.resolve().then //A 这个微任务, 没有产生同步任务,所以执行Promise.resolve().then //A这个微任务 (可能你会产生疑问, Promise.resolve().then //A 会不会进入微任务队列中,其实是会进入微任务队列中的,等Promise.resolve().then //A 产生的同步任务执行完毕后,便会执行其产生的微任务,如果 Promise.resolve().then //A执行过程中还会产生宏任务,也会将宏任务加入到宏任务队列中),执行 Promise.resolve().then //A 这个微任务过程中,会产生同步任务 console.log(‘promise’),便会执行 console.log(‘promise’) ,并输出promise ,然后 Promise.resolve().then //A这个微任务执行完毕。(就是一个 事件循环 的过程)
然后开始开始执行新的宏任务, setTimeout //B 这个宏任务开始执行,会产生 console.log(‘setTimeout’),这个同步任务,执行console.log(‘setTimeout’),并输出setTimeout

所有代码执行结束。

例题3:

<script>
        function yibu() {
            console.log('yibu');
        }

        setTimeout(yibu, 1000);

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

答案:main yibu

例题4:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

答案:1,7,6,8,2,4,3,5,9,11,10,12

(注意:
  new Promise(function(resolve) {
    
   })  为同步任务,
.then(function() {
    console.log('12')
})为微任务
)

第一轮事件循环流程分析如下:

整体script作为第一个宏任务进入主线程,遇到console.log,输出1。

遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。

遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。

遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。

又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。

宏任务队列微任务队列
setTimeout1process1
setTimeout2then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

我们发现了process1和then1两个微任务。

执行process1,输出6。

执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮事件循环从setTimeout1宏任务开始:

首先输出2。

接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。

new Promise立为同步任务,即执行输出4,

.then也分发到微任务Event Queue中,记为then2。

宏任务队列微任务队列
setTimeout2process2
then2

上表是第二轮事件循环宏任务结束时各Event Queue的情况。此时已经输出了1,7,6 ,8,2,4。
我们发现了process1和then1两个微任务。

执行process2,输出3。

执行then2,输出5。

那么,此时第二轮事件循环正式结束,此时结果输出顺序: 1,7,6 ,8,2,4,3,5.那么第三轮事件循环从setTimeout2宏任务开始:

首先输出9.

接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process3。

new Promise立为同步任务,即执行输出11,.

.then

第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。

.then也分发到微任务Event Queue中,记为then3。

宏任务队列微任务队列
process3
then3

上表是第三轮事件循环宏任务结束时各Event Queue的情况。此时已经输出了1,7,6 ,8,2,4,3,5,9,11。
我们发现了process1和then1两个微任务。

执行process3,输出10。

执行then3,输出12。

第三轮事件循环结束。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

参考文章链接:https://juejin.cn/post/6844903638238756878

总结:

这是我第一次发帖子,以后还会发。如果那个地方有错误,可以指出来,我们共同交流。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值