跳到主要内容

1.2-事件循环

Create by fall on 30 Aug 2021 Recently revised in 13 Oct 2023

主要内容:node.js 中所有事件的执行顺序。还有一个例子,对事件的执行顺序进行说明。

事件循环

Event Loop:作为一个 JS 语言的执行模型,不同的地方有不同的实现,node.js 和浏览器以自己不同的方式实现各自的 Event Loop。

JavaScript 代码运行在单个线程上,每次只处理一件事。简化了编程方式,不用考虑并发问题。

只要注意如何编写代码,并且避免任何可能阻塞线程的事情,例如同步的调用或无限的循环即可(编程简单,线程思想简单)。

宏队列微队列

宏队列 macro task:也叫 tasks,一些异步任务回调会进入 macro task,等待后续被调用。包括:

  • setTimeout
  • setInterval
  • I/O
  • setImmediate(仅 Node 有)
  • requestAnimationFrame (浏览器独有)
  • UI rendering (浏览器独有)

微队列 microtask,也叫 jobs。另一些异步会进入微队列 micro task queue,等待被调用,这些包括:

  • process.nextTick(Node 独有)
  • Promise
  • Object.observe(被移除,现在是 Proxy)
  • MutationObserver

大多数浏览器中,每个浏览器选项卡都有一个事件循环,使每个进程都隔开,避免一个网页繁重的处理影响到整个浏览器的网页。

宏队列的执行

  1. 宏队列 macrotask 一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,知道 microtask queue 为空;
  3. 图中没有画 UI rendering 的节点,因为这个是由浏览器自行判断决定的,但是只要执行 UI rendering,它的节点是在执行完所有的 microtask 之后,下一个 macrotask 之前,紧跟着执行 UI render。

阻塞事件循环

任何花费太长时间才能将控制权返回给事件循环的 JavaScript 代码,都会阻塞页面中任何 JavaScript 代码的执行,甚至阻塞 UI 线程,并且用户无法单击浏览、滚动页面等。

JavaScript 中几乎所有的 I/O 基元都是非阻塞的。 网络请求、文件系统操作等。 被阻塞是个异常,这就是 JavaScript 如此之多基于回调(最近越来越多基于 promiseasync/await)的原因。

总结:除 setTimeout 和异步操作外,其它函数执行完成之后,再开始执行计时器或者异步操作的函数

事件循环函数

每当事件循环进行一次完整的行程时,我们都将其称为一个滴答。

process.nextTick()

当将一个函数传给 process.nextTick() 时,则指示引擎在当前操作结束(在下一个事件循环滴答开始之前)时调用此函数。简单来讲,会把函数放置到下一个循环执行

setTimeout() 内的函数会在下一个滴答结束时执行,比使用 nextTick()(其会优先执行该调用并在下一个滴答开始之前执行该函数)晚得多。

process.nextTick(()=>{console.log("199")})
// nextTick内传入的为函数
console.log("pei")
setTimeout(()=>{console.log("999")},0)
// 执行后输出顺序为 pei 199 999

setImmediate()

传入的任何函数都是在事件循环的下一个迭代中执行的回调

延迟 0 毫秒的 setTimeout() 回调与 setImmediate() 非常相似。 执行顺序取决于各种因素,但是它们都会在事件循环的下一个迭代中运行。

某些浏览器实现了setImmidate()的功能,并且其它浏览器上不可用,在node.js中是标准的函数。

setImmediate(()=>{
console.log("996")
})

setTimeout()

定时器的使用

根据HTML5的标准,setTimeout()最少为 4ms

// 第一个传入执行的函数,第二个参数传入事件(单位毫秒)
// 返回值是该定时器的id,通常不会使用,但是可以保存
var id = setTimeout(()=>{console.log("beautiful")},2000) // 如果后面再继续传参,会认为是定时函数的参数
clearTimeout(id)

setInterval()

在指定的时间间隔内执行函数

var ss = setInterval(()=>{
// 执行的函数
},2000)
clearInterval(ss)

事件循环阐明了 Node.js 如何做到异步且具有非阻塞的 I/O。

事件触发器

在后端,可以使用 events 构建类似于前端的系统选项

var EventEmitter = require('events').EventEmitter; 
var event = new EventEmitter();
event.on('some_event', function() {
console.log('some_event 事件触发');
});
setTimeout(function() {
event.emit('some_event');
}, 1000)
// 面试的时候,有让写订阅触发

浏览器内的事件循环

事件循环检测

console.log(1); // 第1个输出,无争议
setTimeout(() => {
console.log(2); // 第5个输出
Promise.resolve().then(() => {
console.log(3)// 第6个输出
});
});
new Promise((resolve, reject) => {
console.log(4) // 第2个输出,输出Promise中非异步程序可以直接执行
resolve(5) // 碰到resolve,先继续执行程序
}).then((data) => {
console.log(data); // 第4个输出,微事件
})
setTimeout(() => {
console.log(6); // 第7个输出
})
console.log(7)// 第3个输出,

第二个作为检测

console.log(1);

setTimeout(() => {
console.log(2);
Promise.resolve().then(() => {
console.log(3)
});
});

new Promise((resolve, reject) => {
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);

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

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

setTimeout(() => {
console.log(9);
})

console.log(10);
正确的输出序列:
1,4,10,5,6,7,2,3,9,8

线程

Node 严格意义讲并非只有一个线程,通常说的 “Node 是单线程” 其实是指 JS 的执行主线程只有一个

进程

  • 具有一定独立功能的程序在一个数据集上的一次动态执行的过程
  • 是操作系统进行资源分配和调度的一个独立单位
  • 是应用程序运行的载体。

线程

  • 程序执行中的一个单一的顺序控制流
  • 存在于进程之中
  • 操作系统调度执行的最小单位

在早期的单核CPU中,为了实现多任务的运行,引入了进程的概念,不同的程序运行在数据与指令相互隔离的进程中,通过时间片轮转调度执行,由于 CPU 时间片切换与执行很快,所以看上去像是在同一时间运行了多个程序。

进程切换时需要保存相关硬件现场、进程控制块等信息,所以系统开销较大,为了进一步提高系统吞吐率,在同一进程执行时更充分的利用 CPU 资源,引入了线程的概念。线程是操作系统调度执行的最小单位,它们依附于进程中,共享同一进程中的资源,基本不拥有或者只拥有少量系统资源,切换开销极小。

子进程

事件循环机制:主程序发生阻塞事件时,会将耗时的操作放到事件队列中,继续执行后续的程序。

事件循环机制使Node实现了I/O密集场景下的高并发,但是遇到CPU密集场景,那么主线程将长时间阻塞,无法处理额外的请求。为了满足CPU密集场景,Node提供了 child_process 模块进行进程的创建、通信、销毁。