Eventloop

98 阅读3分钟

JavaScript有一个基于事件循环的运行时模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。

运行时概念

一个运行时有且只有一个调用栈、消息队列、堆。

image.png

Stack调用栈

  • 当前语句会先进栈,处理完再出栈。
  • 函数调用嵌套回调函数,每一个被调用的函数都会生成一个函数作用域并放置于调用栈顶
  • 处理当前函数的“指针”总是指向栈顶作用域,所以每时每刻都只会处理栈顶的函数作用域
  • 栈顶函数作用域处理完后“指针”依次下移,栈顶作用域失去“指针”后,会被浏览器垃圾回收机制标记成为待回收数据或直接清除。
    function foo(b) {
      const a = 10;
      return a + b + 11;
    }

    function bar(x) {
      const y = 3;
      return foo(x * y);
    }

    const baz = bar(7); // 将 42 赋值给 baz

上面的示例方法中,当前函数栈从顶到底压入了foo/bar两个函数的状态帧。具体执行顺序为:

  1. 当调用 bar 时,第一个帧被创建并压入栈中,帧中包含了 bar 的参数引用和局部变量。
  2. 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数引用和局部变量。
  3. 当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧)。
  4. 当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了。

Heap堆

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

Queue队列

  • JavaScript运行时包含一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。
  • 一个消息会激活一个函数栈,并开始栈的堆叠,直到所有的回调函数都触发后,栈里面的帧开始逐个被清除,直到栈被清空,当前消息就算执行完成。
  • 每个消息中可能有宏任务与微任务被触发,当前消息队列中的微任务会按照触发顺序排列在消息队列末尾,和当前消息队列的任务一同执行;宏任务将生成一个新的消息队列,并根据延迟时间安插最近的消息队列后执行。

事件循环概念

事件循环的执行原理:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

事件循环的一些注意事项:

  • 完整执行:每一个消息完整地执行后,其他消息才会被执行。
    • 优点:当一个函数执行时,不需要考虑这段函数所使用的参数被其他函数同时使用着,可以直接读、写需要的参数。
    • 缺点:单线程模式,遇到占用时间长的消息,将会彻底阻塞当前应用,直到该消息完成后,才能继续进行应用流程。
  • 引起消息添加的事件:事件监听器被触发、setTimeout计时器到期之后回调函数被执行。
  • setTimeout的0ms延迟:等待时间并不为0,而是取决于队列里待处理的消息数量。
  • 多个运行时如何互相通信:Web worker或者跨源的iframe都有自己的栈、堆和消息队列。两个不同的运行时只能通过postMessage方法进行通信。如果另一个运行时侦听message事件,则此方法会向该运行时添加消息。

事件循环的永不阻塞

由于事件循环的异步任务总是会给同步任务让行,所以在一定程度上是“永不阻塞的”,也就是每一份代码总能被执行,除非代码报错了。