学习链接

浏览器与Node的事件循环(Event Loop)有何区别?

深入理解js事件循环机制(浏览器篇)

深入理解js事件循环机制(Node.js篇)

stackoverflow高赞回答1

stackoverflow高赞回答2

Tasks, microtasks, queues and schedules(动画👍)

规范

网道:异步操作概述

事件循环:微任务和宏任务

一个观察代码运行过程的网站

事件循环

同步任务和异步任务

JavaScript 的任务分为同步任务(synchronous)和异步任务(asynchronous)两种:

(“任务”不一定单指某条语句的执行)

  • 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务
    • 只有前一个任务执行完毕,才能执行后一个任务
  • 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务
    • 只有引擎认为某个异步任务可以执行了,该任务才会进入主线程执行
    • 排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应

再具体一些:

  • 同步任务会被放到调用栈(call stack)中排队,主线程会去执行调用栈中的任务
  • 异步任务会被放到任务队列(task queue)排队等待
    • 等到调用栈中的同步任务全部执行完后,就会去任务队列中寻找满足条件的异步任务
    • 将满足条件的异步任务推入调用栈中,此时变为同步任务,主线程会执行调用栈中的任务
    • 执行完后,继续去任务队列中寻找下一个符合要求的异步任务

引擎不断地检查调用栈是否为空,以及确定把哪个异步任务推入调用栈的这个过程就是事件循环(Event Loop)。

浏览器中的事件循环

  • 浏览器至少有一个事件循环
  • 一个事件循环有一个或多个宏任务队列(macrotask queue)
  • 一个事件循环只有一个微任务队列(microtask queue)
  • 一个任务可能会被推入宏任务队列或微任务队列
  • 当一个任务被推入某队列 (micro/macro) 时,则认为该任务的准备工作都已完成,可被立即执行

事件循环

调用栈为空时,执行步骤👇

  1. 宏任务队列中选取排在最前面的一个任务,推入调用栈中执行,执行完后移出队列
  2. 若宏任务队列一开始就为空,则直接👉3
  3. 清空微任务队列
    1. 在微任务队列中选取排在最前面的一个任务,推入调用栈中执行,执行完后移出队列
    2. 若微任务队列为空,则👉4
    3. 继续选择下一个排在最前面的微任务👉3.1
    4. 直到微任务队列被清空
  4. 执行渲染操作,更新界面(未必每次都更新,优化:累计更新)
  5. 检查是否存在 Web worker 任务,如果有,则对其进行处理
  6. 继续选择下一个排在最前面的宏任务👉1
  7. 宏任务队列被清空

当调用栈为空时,

将最前面的一个宏任务推入调用栈中执行,

然后再将一队微任务逐个推入调用栈中执行(即清空微任务队列),

接着,进行视图的渲染(未必每轮都执行),

然后进入下一轮事件循环,开始处理下一个宏任务,

直到宏任务队列和微任务队列都被清空。

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

宏任务与微任务

  • 常见的宏任务:setTimeout、setInterval、script (整体代码)、事件回调、XHR 回调、I/O 操作、UI 渲染等。(setImmediate——node.js中的实现)(事件的触发?
  • 常见的微任务:Promise 回调、MutationObserver (html5新特性) 等。(process.nextTick——node.js中的实现)

注意:每个微任务都可产生新的微任务(如此递归可能导致卡死)

安排(schedule)一个新的 宏任务

  • 使用零延迟的 setTimeout(f)

它可被用于将繁重的计算任务拆分成多个部分,以使浏览器能够对用户事件作出反应,并在任务的各部分之间显示任务进度。

此外,也被用于在事件处理程序中,将一个行为(action)安排(schedule)在事件被完全处理(冒泡完成)后。

安排一个新的 微任务

  • 使用 queueMicrotask(f)
  • promise 处理程序也会通过微任务队列。

在微任务之间没有 UI 或网络事件的处理:它们一个立即接一个地执行。

所以,我们可以使用 queueMicrotask 来在保持环境状态一致的情况下,异步地执行一个函数。

setTimeout 和 setInterval

setTimeoutsetInterval 的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行指定的代码;如果不到,就继续等待。

这意味着,setTimeoutsetInterval 指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才可能开始执行

由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval 指定的任务,一定会按照预定时间执行。

Node 中的事件循环

https://github.com/ljianshu/Blog/issues/54

http://lynnelv.github.io/js-event-loop-nodejs

  1. Node.js 的事件循环分为6个阶段
  2. 浏览器和 Node 环境下,microtask 任务队列的执行时机不同
    • Node.js中,microtask 在事件循环的各个阶段之间执行
    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

Web Worker

https://wangdoc.com/javascript/bom/webworker.html

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务可以交由 Worker 线程执行,主线程(通常负责 UI 交互)能够保持流畅,不会被阻塞或拖慢。

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。