# js 事件循环机制

# JAVASCRIPT 为什么是单线程的?

  • 作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM,决定了 js 只能为单线程
  • 比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

TIP

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质

# 什么是异步

什么是异步

代码在执行过程中,会遇到一些无法立即处理的任务,比如以下;

  • 计时完成后需要执行的任务 -- setTimeout、setInterval
  • 网络通信完成后需要执行的任务 -- XHR、Featch
  • 用户操作后需要执行的任务 -- addEventListener

如果让渲染线程等待这些任务的执行时机没达到、就会导致主线程长期处于【阻塞】的状态,从而导致浏览器【卡死】

# 为什么需要异步?

单线程只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞,不向下执行。会造成页面卡死现象。

面试题:如何理解js中的异步?

  • Js是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程又承担着诸多工作,例如渲染页面【布局、计算样式、处理图层】、事件处理、执行js、回调、解析HTML等等。 如果使用同步的方式,就极有可能使主线程,产生阻塞,从而使消息队列中的很多其他任务无法执行。这样一来主线程中就会因为阻塞浪费时间。 导致页面无法及时更新,造成页面卡顿的现象。
  • 所以浏览器采用异步的方式避免,具体做法就是当某些任务例【计时器、网络请求、事件监听】主线程将任务交给其他线程去处理,自身立即结束任务的执行, 从而去执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调用执行。
  • 在这种异步的模式下,浏览器不阻塞,最大限度的保证了主线程的流畅运行。

# 单线程又是如何实现异步的呢?

通过事件循环实现异步

事件循环

::: tip 面试:简述事件循环

  • 事件循环又叫消息循环,是浏览器主线程的工作方式。
  • 在Chrome源码中, 开启了一个不会结束的for循环,每次循环从消息队列中取出第一个任务,而其他线程只需要在合适的时间将任务添加到队列末尾即可。
  • 过去把消息队列分为宏队列和微队列,根据W3c最新的解释来看,每个任务有不同的类型,同类型的任务必须在同一个队列中,不同的任务可以属于不同的队列。
  • 不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务,但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行 :::

# 宏任务&微任务

宏任务(macro-task) 微任务(micro-task)
整体的script代码 promise.[ then/catch/finally ]((非new Promise))
setTimeout process.nextTick(Node.js 环境)
setInterval MutaionOberver(浏览器环境)
setImmediate(Node.js 环境) Object.observe
IO操作 x
UI交互事件 x
postMessage x
MessageChannel x

# 宏任务(macro-task)

整体代码 script、setTimeOut、setInterval、setImmediate

TIP

setImmediate()只有一个的时候永远最后执行,多个的时候每次循环只执行一个

# 微任务(micro-task)

Promise 的回调(promise.then) 、async/await、process.nextTick()永远最先执行,存在多个的时候先进先出

# 单个 setImmediate()

setTimeout(function timeout() {
  console.log("setTimeout");
}, 0);
setImmediate(function A() {
  console.log(1);
});
process.nextTick(function A() {
  console.log(2);
});
new Promise(function(resolve) {
  console.log("3");
  resolve();
}).then(function() {
  console.log("4");
});
// 输出 3 2 4 setTimeout 1

分析执行过程

  • 1、setTimeout 异步任务进入 event table
  • 2、setImmediate 异步任务进入 event table
  • 3、process.nextTick 异步任务进入 event table
  • 4、promise 同步任务执行 console.log(3), .then回调异步任务 进入event table
  • 5、此时同步任务执行完毕,优先执行微任务 process.nextTick 该任务被推入 event loop ,经判断 主线程为空,则进入主线程执行。
  • 6、then()回调函数 为微任务 等待 process.nextTick 结束 执行
  • 7、执行宏任务 setTimeout
  • 8、setImmediate 最后执行

# 多个 setImmediate()

setTimeout(function timeout() {
  console.log("setTimeout");
  setImmediate(function() {
    console.log(9);
  });
}, 0);
setImmediate(function() {
  console.log(1);
  setImmediate(function() {
    console.log(7);
  });
});
process.nextTick(function() {
  setImmediate(function() {
    console.log(6);
  });
  console.log(2);
});
new Promise(function(resolve) {
  console.log("3");
  resolve();
}).then(function() {
  console.log("4");
});
setImmediate(function() {
  console.log(5);
});

// 输出结果:  3 2 4 setTimeout 1 5 6 9 7

分析执行过程

  • 1、setTimeout宏任务进入 event table 记作setTimeout1
  • 2、setImmediate 微任务 进入 event table 记作setImmediate1
  • 3、process.nextTick 微任务 进入 event table 记作 process1
  • 4、promise 执行 console.log(3) then()回调 微任务 进入 event table记作 promise1
  • 5、setImmediate 微任务 进入 event table 记作setImmediate2 event table:
    宏任务 微任务
    setTimeout1 setImmediate1
    process1
    promise1
    setImmediate2

第一轮执行结束输出 3

第二轮开始

  • 1、process.nextTick 在第一轮执行过程中已经 存在 event table了,由于它在循环开始执行,故优先执行 process1
    • 1、 setImmediate 微任务进入 event table记作setImmediate3
    • 2、 执行 console.log(2)
  • 2、执行当前event table中的 微任务 promise1 的 then()回调函数console.log(4)
  • 3、执行当前event table中的 宏任务 setTimeout1
    • 1、 执行 console.log(‘setTimeout’)
    • 2、setImmediate 进入 event lable记作setImmediate4`
  • 4、setImmediate 最后执行 故执行 setImmediate1
    • 1、执行 console.log(1)
    • 2、setImmediate 进入 event lable 记作 setImmediate5
  • 5、event table 中还有 setImmediate2 执行 console.log(5)
    宏任务 微任务
    setImmediate3
    setImmediate4
    setImmediate5

第二轮 执行结束 结果为: 2 4 setTimeout 1 5; 进入第三轮

  • 1、执行 setImmediate3 中 console.log(6)
  • 2、执行 setImmediate4 中 console.log(9)
  • 3、执行 setImmediate5 中 console.log(7)

第三轮 的执行结果为 6 9 7 至此 该程序执行完毕

# 执行顺序

WARNING

  • 1、渲染主线程
  • 2、微队列
  • 3、延时队列
  • 4、交互队列
  • 1、同步任务
  • 2、promise.nextTick (Node 独有)
  • 3、微任务
  • 4、Dom 渲染
  • 5、宏任务
  • 6、setImmediate (Node 独有)
Last Updated: 3/2/2023, 11:14:14 AM