基于真实案例浅谈事件循环EventLoop

455 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

前情提要

为什么说是浅谈呢?因为EventLoop的牵扯到的情况特别复杂也特别的庞大,一句两句讲不清楚,可能讲着讲着把自己就给讲糊涂了。所以我们就由表及里,以结果为导向看看事件循环的运行机制。

什么是事件循环?

事件循环的本质是在浏览器或者nodejs的环境中,运行时对js脚本的调度方式。

  1. 首先我们先了解下js为什么会有事件循环机制?
    • 我们都知道JavaScript是单线程的。所谓单线程,就可以理解为一个人的心一段时间只能放下一个人,分开了才能去存放另一个人,无法一心二用。但是如果按照这种情况走下去,假如代码块中遇到了计时器5秒甚至50秒后触发,那么整个程序都会因为还没有到程序的执行时间而停滞不前,进而进入假死状态。这种情况自然是我们所不希望看到的,我们希望遇到此类需要等待的代码时跳过去,先执行不需要等待的代码,最后再来执行这些需要等待的代码。于是JavaScript就引入了事件循环机制,来模拟多线程的效果。
    • 而实现这种效果的方式就是事件循环EventLoop机制
  2. 其次是如何实现这种机制
    • 该机制将js分为同步任务和异步任务。
      • 同步任务为一个任务执行栈(栈的规则是先进后出)
      • 异步任务是一个消息队列(消息队列的规则是先进先出)。
      • 同时还有浏览器提供的webApi,可以理解为浏览器自己提供的api会在另一个线程处理,处理完成后会自动塞到js的消息队列里。
      • 消息队列又分为宏任务队列和微任务队列。
    • 浏览器提供的api是宏任务:setTimeoutsetIntervalsetImmediateajaxnextTickrequestAnimationFrameUI render...
    • js引擎提供的是微任务:promise, async/await...
    • script中的js代码也是宏任务
  3. 执行优先级
    • 同步任务 => nextTick => 异步任务 => setImmediate

从代码执行验证上述理论

  • 代码加分析、
    // 执行环境是node,不是浏览器哦
    console.log(1); // 同步任务,立即执行。控制台:1
    setTimeout(() => { // webapi线程处理,延迟为0,立即压入宏任务队列。
      console.log(2) // 同步任务,立即执行。控制台:1 4 6 7 11 110 2
    },0)
    const a = new Promise((resolve, reject) => {
      console.log(4) // 同步任务,立即执行。控制台:1 4
      setTimeout(() => { // webapi线程处理,延迟为0,立即压入宏任务队列。
        resolve(5)
      }, 0)
    })
    function b() { // 函数声明,等待被调用
      console.log(6) // 同步任务,立即执行。控制台:1 4 6
      return new Promise((resolve, reject) => {
        console.log(7) // 同步任务,立即执行。控制台:1 4 6 7
        setTimeout(() => { // webapi线程处理,100毫秒后压入宏任务队列。
          resolve(8)
        }, 100)
      })
    }
    process.nextTick(function () { // nickTick在宏任务处理结束后开始执行
      console.log(11) // b方法执行结束后当前宏任务处理结束。开始执行nextTick。同步任务,立即执行。控制台:1 4 6 7 11
      const aaa = new Promise((resolve, reject) => {
          console.log(110) // 同步任务,立即执行。控制台:1 4 6 7 11 110
          setTimeout(() => { // webapi线程处理,延迟为0,立即压入宏任务队列。
            reject(444)
          },0)
      })
      aaa.catch((err) => {console.log(err)}) // 压入微任务队列。
    });
    b().then((res) => { // 先执行b方法。 // 由于b的setTimeout延迟为100,所以会晚于a执行
      console.log(9) // 同步任务立即执行。控制台:1 4 6 7 11 110 10 5 9 
      console.log(res) // 同步任务立即执行。控制台:1 4 6 7 11 110 10 5 9 8
    })
    a.then((res) => { // 微任务。第一轮宏任务结束后,由于此时promise还未返回resolve或者reject,所以略过。等第二轮宏任务,第三轮宏任务开始才返回。
      console.log(10) // 此时,同步任务,立即执行。控制台:1 4 6 7 11 110 10
      console.log(res) // 同步任务,立即执行。控制台:1 4 6 7 11 110 10 5
    })
    
  • 控制台打印顺序: WX20220527-150900@2x.png
  • 注意事项:
    • process.nextTick只在node环境中生效。

总结

  • 在浏览器中事件循环除了js的处理还有ui线程的处理。每走完一轮js的便会走一遍ui的处理。以此往复形成事件循环,但ui线程那块暂时想不到演示示例,所以目前只演示了js这一块。
  • 其实最一开始案例代码中有setImmediate的存在。但是经过多次调用发现,打印结果有些不稳定。故在下一篇文章进行详细探讨。