浏览器事件循环与Node事件循环

266 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

前言

我们都知道JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。本篇主要总结归纳浏览器和node事件循环,如有写的不准确的地方,欢迎大家指出,相互学习,相互进步!

一. 浏览器事件循环

大多数浏览器其实都是多进程的,

当我们打开一个tab页面时就会开启一个新的进程(为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出)

每个进程中又有很多的线程,其中包括执行JavaScript代码的线程, 但是JavaScript的代码执行是在一个单独的线程中执行,同一个时刻只能做一件事,因此会导致非常耗时和堵塞的情况。所以这些就会让浏览器的其他线程完成这个操作,比如网络请求、定时器

提示:Event Loop包括了 执行栈, 任务队列, 微任务,宏任务,执行栈和任务队列是事件循环中存储事件的地址,微任务和宏任务是放在任务队列里的异步事件

1.宏任务和微任务

浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,虽然浏览器不同,不过其内部的事件循环规则是一致的。

console.log("script start")
// 业务代码
setTimeout(function() {
  console.log('HHH')

}, 1000)

console.log("~")
console.log("script end")

image.png

以上代码看出:setTimeout函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行。 但是事件循环中并非只维护着一个队列,事实上是有两个队列:

宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering、I/O 操作等

微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等

两个队列的优先级: 同一次事件循环中,微任务永远在宏任务之前执行

也就是宏任务执行之前,必须保证微任务队列是空的,如果不为空,那么就优先执行微任务队列中的任务(回调)。

举例:

setTimeout(() => {
  console.log("setTimeout")
}, 1000)

//微任务队列
queueMicrotask(() => {
  console.log("queueMicrotask")
})

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

function foo() {
  console.log("foo")
}

function bar() {
  console.log("bar")
  foo()
}

bar()
console.log("其他代码")

image.png

2.事件循环顺序

image.png

image.png

练习题:

async function bar() {
  console.log("22222")
  return new Promise((resolve) => {
    resolve()
  })
}

async function foo() {
  console.log("111111")

  await bar()

  console.log("33333")
}

foo()
console.log("444444")

image.png

二. Node事件循环

Node事件循环处理方面采用了libuv来实现的。

libuv:一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等

Node的架构图:

image.png

我们会发现libuv中主要维护了一个EventLoop(负责调用系统的一些其他操作:文件的IO、Network、child-processes等)和worker threads(线程池)

一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED。
  • pidle, prepare: 仅系统内部使用。
  • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;
  • 检测(check):setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

官网Node事件循环操作顺序的简化概览:具体描述点击官网学习nodejs.org/zh-cn/docs/…

image.png

1.宏任务和微任务

从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;

微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

微任务队列:

next tick queue:process.nextTick;

other queue:Promise的then回调、queueMicrotask;

宏任务队列:

timer queue:setTimeout、setInterval;

poll queue:IO事件;

check queue:setImmediate;

close queue:close事件;

2.事件循环顺序

  1. next tick microtask queue;
  2. other microtask queue;
  3. timer queue;
  4. poll queue;
  5. check queue;
  6. close queue;

举例:

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(function () {
  console.log('setTimeout0')
}, 0)

setTimeout(function () {
  console.log('setTimeout2')
}, 300)

setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
  console.log('promise1')
  resolve();
  console.log('promise2')
}).then(function () {
  console.log('promise3')
})

console.log('script end')

image.png

总结

本篇文章并没有细致的介绍,如有个别概念看不懂或想深入理解的推荐大家阅读这几篇文章进行一个更细致的学习

  1. node事件循环详解

  2. 形象化解读js事件循环以及node事件循环和浏览器事件循环的区别

  3. 面试题:说一说JS实现异步的方法?

  4. 面试题:事件循环JS题目