资讯专栏INFORMATION COLUMN

理解javascript中的事件循环(Event Loop)

mykurisu / 3391人阅读

摘要:主线程会暂时存储等异步操作,直接向下执行,当某个异步事件触发时,再通知主线程执行相应的回调函数,通过这种机制,避免了单线程中异步操作耗时对后续任务的影响。

背景

在研究js的异步的实现方式的时候,发现了JavaScript 中的 macrotask 和 microtask 的概念。在查阅了一番资料之后,对其中的执行机制有所了解,下面整理出来,希望可以帮助更多人。

先了解一下js的任务执行机制

首先,javascript是单线程的,所以只能通过异步解决性能问题(否则,如果前面一个任务阻塞了,那么后续的任务都要等待,这种效果是无法接受的)。js在执行代码时存在着两个比较重要的东西:执行栈和任务队列,这两个东西都是用来存储任务的,区别在于:执行栈里面存着的都是同步任务,也就是要按顺序执行的任务;而任务队列中存着的是一些异步任务,这些异步任务一定要等到执行栈清空后才会执行(这句话很重要)。关于任务队列,它还分成两种,一种叫作macrotask queue(姑且这么命名,因为严格来说规范中只有说task,并没有提到macrotask这个概念。这里为了容易区分,可以理解为macrotask=task!=microtask),另一种叫作microtask queue。如果同时考虑node环境和浏览器环境的话,这两种任务分别对应以下api:
microtasks:

process.nextTick

promise

Object.observe

MutationObserver

macrotasks:

setTimeout

setInterval

setImmediate

I/O

UI渲染

script标签中的整体代码

javascript在执行时,先从 macrotasks 队列开始执行,取出第一个 macrotask 放入执行栈执行,在执行过程中,如果遇到 macrotask,则将该 macrotask 放入 macrotask 队列,继续运行执行栈中的后续代码。如果遇到microtask,那么将该microtask放入microtask队列,继续向下运行执行栈中的后续代码。当执行栈中的代码全部执行完成后,从microtasks队列中取出所有的microtask放入执行栈执行。执行完毕后,再从macrotasks 队列取出下一个macrotask放入执行栈。然后不断重复上述流程。这一过程也被称作事件循环(Event Loop)。
javascript就是通过这种机制来实现异步的。主线程会暂时存储I/O等异步操作,直接向下执行,当某个异步事件触发时,再通知主线程执行相应的回调函数,通过这种机制,javascript避免了单线程中异步操作耗时对后续任务的影响。

图解事件循环流程

根据图中描述,一次事件循环的执行步骤如下:
1、从macrotask queue中取出最早的任务
2、在执行栈中执行第一步取出的任务
如果任务中存在microtask,将其压入到microtask queue中
如果任务中存在macrotask,将其压入到macrotask queue中
直到执行完毕
3、执行栈设置为null
4、从macrotask queue中删除执行过的macrotask
5、取出microtask queue中的全部任务,放入执行栈,
如果任务中存在microtask,将其压入到microtask queue中
如果任务中存在macrotask,将其压入到macrotask queue中
注意:这里产生的microtask(也就是microtask产生的microtask )也会在这一步骤中执行。
直到当前microtask queue为空,此步骤结束。
6、执行第一步的操作

实例验证

我们执行如下一段代码,用上面的思路执行,看一下结果是否和预期的一致。

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")
    })

按照上面的思路,我们来理一下,预测一下执行结果,看看实际效果是否是这样的。
执行流程:
第一轮:
1、首先这一整段js代码作为一个macrotask先被执行
2、遇到console.log("start"),输出start
3、遇到setInterval,回调函数作为macrotask压入到macrotask queue中,
此时macrotask queue:[setInterval]
4、遇到setTimeout,回调函数作为macrotask压入到macrotask queue中,
此时macrotask queue:[setInterval,setTimeout1]
5、遇到Promise,并且调用了resolve方法,触发了回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 1,promise 2]
6、执行栈为空,将microtask queue中的任务放入执行栈
7、执行microtask queue中Promise的回调任务,分别打印promise 1,promise 2
8、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
目前macrotask queue:[setInterval,setTimeout1]

第二轮:
1、从macrotask queue中取出最早的任务,这里对应的是第一轮中第3步的回调函数:console.log("setInterval"),输出setInterval
2、setInterval的回调函数作为macrotask压入到macrotask queue中
此时macrotask queue:[setTimeout1,setInterval]
3、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
目前macrotask queue:[setTimeout1,setInterval]

第三轮:
1、从macrotask queue中取出最早的任务,目前是setTimeout1的回调,将取出的任务放入执行栈执行
2、遇到console.log("setTimeout 1"),输出setTimeout 1
3、遇到Promise,并且调用了resolve方法,触发回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 3,promise 4,() => {setTimeout 2}]
4、执行栈为空,将microtask queue中的任务放入执行栈
5、执行microtask queue中Promise的回调任务:
输出promise 3
输出promise 4
将setTimeout 2压入macrotask queue
6、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
目前macrotask queue:[setInterval,setTimeout2]

第四轮:
1、从macrotask queue中取出最早的任务,这里对应的是setInterval,输出setInterval
2、setInterval的回调函数作为macrotask压入到macrotask queue中
此时macrotask queue:[setTimeout2,setInterval]
3、执行栈为空,microtask queue为空,开始下一轮事件循环
目前的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
目前macrotask queue:[setTimeout2,setInterval]

第五轮:
1、从macrotask queue中取出最早的任务,目前是setTimeout2的回调,将取出的任务放入执行栈执行
2、遇到console.log("setTimeout 2")输出setTimeout 2
3、遇到Promise,并且调用了resolve方法,触发回调,回调作为microtask压入到microtask queue中
此时microtask queue:[promise 5,promise 6,() => {clearInterval}]
4、执行栈为空,将microtask queue中的任务放入执行栈
5、执行microtask queue中Promise的回调任务:
输出promise 5
输出promise 6
clearInterval清空setInterval计时器
6、执行栈为空,microtask queue为空,macrotask queue为空,任务结束。
最终的console中打印内容:
start
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
promise 5
promise 6

通过图片可以看到,结果跟我们的预期一致,在promise2的后面作为方法的返回值,多打印了一个undefined,这个应该好理解的。

这里面有个小问题,就是在不同的环境下(node/浏览器),promise4后面的setInterval表现可能会有差异,这里可能跟setTimeout和setInterval的最小间隔有关,虽然我们写成0ms,但实际上这个最小值是有限制的,现阶段不同组织和不同的js引擎实现机制存在差异,不过这个问题不在本次讨论范围之内了。如果我们将上述代码中setInterval的间隔设置为10,那么整个执行流程将严格符合我们的预期。

有什么用?

后续我们在代码中使用Promise,setTimeout时,思路将更加清晰,用起来更佳得心应手。

在阅读一些源码时,对于一些setTimeout相关的骚操作可以理解的更加深入。

理解javascript中的任务执行流程,加深对异步流程的理解,少犯错误。

总结

js事件循环总是从一个macrotask开始执行

一个事件循环过程中,只执行一个macrotask,但是可能执行多个microtask

执行栈中的任务产生的microtask会在当前事件循环内执行

执行栈中的任务产生的macrotask要在下一次事件循环才会执行

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107861.html

相关文章

  • 初窥JavaScript事件机制的实现(一)—— Node.js事件驱动实现概览

    摘要:如果当前没有事件也没有定时器事件,则返回。相关资料关于的架构及设计思路的事件讨论了使用线程池异步运行代码。下一篇初窥事件机制的实现二中定时器的实现 在浏览器中,事件作为一个极为重要的机制,给予JavaScript响应用户操作与DOM变化的能力;在Node.js中,事件驱动模型则是其高并发能力的基础。 学习JavaScript也需要了解它的运行平台,为了更好的理解JavaScript的事...

    lavor 评论0 收藏0
  • JavaScript Event loop 事件循环

    摘要:阶段有两个主要功能也会执行时间定时器到达期望时间的回调函数执行事件循环列表里的函数当进入阶段并且没有其余的定时器,那么如果事件循环列表不为空,则迭代同步的执行队列中的函数。如果没有,则等待回调函数进入队列并立即执行。 Event Loop 本文以 Node.js 为例,讲解 Event Loop 在 Node.js 的实现,原文,JavaScript 中的实现大同小异。 什么是 Eve...

    luckyyulin 评论0 收藏0
  • FE.ES-理解Event Loop

    摘要:新加了一个微任务和一个宏任务在当前执行栈的尾部下一次之前触发回调函数。阶段这个阶段主要执行一些系统操作带来的回调函数,如错误,如果尝试链接时出现错误,一些会把这个错误报告给。 JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不同,Event loop也有不同的实现:其中Node使用了libu...

    longshengwang 评论0 收藏0
  • Javascript事件循环入门到“忘记”(一).md

    摘要:中叫做调用栈先进后出,后进先出。如下图这是典型的内存溢出,可能会出现在某些场景下需要递归,但业务逻辑中的判断又没能正常计算进入到预设情况,于是调用栈中不断进入,又无法执行完,就造成内存溢出了。 本文主要介绍Javascript事件循环在浏览器上的一些特性和应用介绍。 Javascript小知识 JavaScript的并发模型基于事件循环(Event Loop)。这个模型与像C或者Jav...

    codeGoogle 评论0 收藏0
  • 浅析 JS 中的 EventLoop 事件循环(新手向)

    摘要:同时,如果执行的过程中发现其他函数,继续入栈然后执行。上面我们讨论的其实都是同步代码,代码在运行的时候只用调用栈解释就可以了。 序 Event Loop 这个概念相信大家或多或少都了解过,但是有一次被一个小伙伴问到它具体的原理的时候,感觉自己只知道个大概印象,于是计划着写一篇文章,用输出倒逼输入,让自己重新学习这个概念,同时也能帮助更多的人理解它~ 概念 JavaScript 是一门 ...

    chadLi 评论0 收藏0
  • JavaScript执行机制、事件循环

    摘要:曾经的理解首先,是单线程语言,也就意味着同一个时间只能做一件事,那么为什么不是多线程呢这样还能提高效率啊假定同时有两个线程,一个线程在某个节点上编辑了内容,而另一个线程删除了这个节点,这时浏览器就很懵逼了,到底以执行哪个操作呢所以,设计者把 Event Loop曾经的理解 首先,JS是单线程语言,也就意味着同一个时间只能做一件事,那么 为什么JavaScript不是多线程呢?这样还能提...

    rose 评论0 收藏0

发表评论

0条评论

mykurisu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<