浏览器的事件循环(Event Loop)

Event Loop (事件循环)是一个很重要的概念,指的是计算机系统的一种运行机制。 JavaScript 采用的就是这种机制,来解决单线程运行带来的一些问题。

JavaScript 是一种单线程语言,所有任务都是在一个线程上执行,如果一个任务等待时间长,会在成页面“假死”。如果采用多线程,会占用多倍的系统资源,也闲置多倍的资源。 Event Loop 就是为了解决这个问题提出的。

“Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”

简单点说,就是在程序中设置两个线程:一个负责程序本身的运行,成为“主线程”;另一个负责主线程和其他进程(主要是各种I/O操作)的通信,被称为“ Event Loop 线程”。

Event Loop 本质上是一个 user agent 上协调处理各类事件的机制。各种浏览器的事件同时触发时,肯定有一个先来后到的排队问题。解决这些事件如何排队触发的机制,就是 Event Loop。这个排队行为在开发人员来看,可以分为两个队列:

  • 外部队列(宏任务/MacroTask):主要是浏览器协调的各类事件的队列,标准文件中称之为 Task Queue
  • 内部队列(微任务/MicroTask): JavaScript 内部执行的任务队列,标准文件中称之为 MicroTask Queue

注:为了更好理解我们称之为队列,其本质是一个有序集合。因为传统的队列是 FIFO (先进先出)的,而这里的队列到了前面如果为满足执行条件也是不会执行的。

外部队列

  • DOM 操作(页面渲染)
  • 用户交互(鼠标、键盘事件)
  • 网络请求( ajax 等)
  • History API 操作事件
  • 定时器( setTimeout/setInterval 等)

内部队列

  • Promise 的 thencatch 方法
  • MutationObserver (监听 DOM 的更改)
  • Object.observe (已废弃,用于监听对象的修改,再调用回调,现在可以用 Proxy 代替)

注:除进入内部队列和外部队列的代码仍旧按照顺序执行。

setTimeout(() => {
  console.log('setTimeout1')
})

Promise.resolve().then(() => {
  console.log('promise1')
})

setTimeout(() => {
  console.log('setTimeout2')
})

Promise.resolve().then(() => {
  console.log('promise2')
})

Promise.resolve().then(() => {
  console.log('promise3')
})

console.log('script end');
// script end - promise1 - promise2 - promise3 - setTimeout1 - setTimeout2

JavaScript 在浏览器端的执行顺序是:

  1. 一次外部事件
  2. 所有内部事件
  3. HTML 渲染
  4. 回到第一

注:一次 script 脚本的加载就是一个外部事件,所以 JavaScript 的解析仍然是按照这个顺序执行。

setImmediate

setTimeout 延迟的时间精度是毫秒( ms ),但实际上事件循环的时间可能是纳秒级的,所以一次事件循环的多个外部队列中,找到某一个队列直接执行其中的 callback 可以得到比 setTimeout 更早执行的效果。因此为了解决时间精度问题, JavaScript 引入了 setImmediate

注:setImmediate 只在 Node.js 中生效,在浏览器端不生效。

Node.js 中的 Event Loop

const fs = require('fs');

setImmediate(() => {
  console.log('setImmediate1');
});

fs.readDir(__dirname, () => {
  console.log('fs.readDir');
});

setTimeout(()=>{
  console.log('setTimeout1');
}, 0);

Promise.resolve().then(() => {
  console.log('promise');
});

setImmediate(() => {
  console.log('setImmediate2');
});

setTimeout(()=>{
  console.log('setTimeout2');
}, 200);

console.log("end");

// end - promise - setTimeout1 - fs.readDir - setImmediate1 - setImmediate2 - setTimeout2

Node.js 中, JavaScript 的执行顺序稍有变化,在 Node.js 10.x 以上的版本运行(因为早些版本,运行顺序和浏览器不一致,早期的 Node.js 允许一次执行多个外部事件,而浏览器端,一次只允许执行一个外部事件)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MKclqWwQ-1606293606743)(./img/node-queue-all.png)]

各阶段描述:

  • timers

    处理 setTimeoutsetInterval 回调,开始处理的时机与 poll 阶段有关联。

  • pending callbacks

    执行某些系统操作的回调。

  • idle, prepare

    内部使用,忽略。

  • poll:

    poll 是一个核心阶段,等新 I/O 事件的触发,以及执行 I/O 相关回调。 Node.js 中出现异步的绝大部分情况都是 I/O 操作,它们的回调基本都在这个阶段被执行。

    poll 阶段主要做两件事:

    • 计算需要为新的的I/O事件等待多久

      当进入poll阶段,如果队列为空且不存在setImmediate与就绪的timer,Node.js会在这里block一定的时间等待新的I/O事件到来,然后立即执行其回调。这种情况具体block等待多久是不具体的,但如果在block一定时间后仍没有新到达的I/O事件,可以肯定循环依旧会进入check阶段或者回到timer阶段。

    • 处理该阶段队列中的事件

      当进入poll阶段,如果队列不为空且没有就绪的timer,Node.js会在这里执行队列中的callback直到队列为空或者执行的callback数达到系统设定的某个值。随后Node.js检查是否存在预设的setImmediate,存在话就进入check阶段,否则开始检查timer就绪情况选择回到timer阶段或者进入check阶段。

  • check

    该阶段的执行内容是所有 setImmediate 回调。

  • close callbacks

    socket的异常关闭,close 事件的回调会在该阶段执行。

这篇文章是在理解 Event Loop 的执行顺序过程中总结而来,但在 Node 端的一个事件循环理解还不是十分透彻,也有去阅读一些其他文献,但是笔者目前在 Node 端具体代码应用还不太多,无法更深入地进行总结,如果有比较好的文章,欢迎推荐,一起学习!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值