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
大多数浏览器中,每个浏览器选项卡都有一个事件循环,使每个进程都隔开,避免一个网页繁重的处理影响到整个浏览器的网页。
宏队列的执行
- 宏队列 macrotask 一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 微任务队列中所有的任务都会被依次取出来执行,知道 microtask queue 为空;
- 图中没有画 UI rendering 的节点,因为这个是由浏览器自行判断决定的,但是只要执行 UI rendering,它的节点是在执行完所有的 microtask 之后,下一个 macrotask 之前,紧跟着执行 UI render。
阻塞事件循环
任何花费太长时间才能将控制权返回给事件循环的 JavaScript 代码,都会阻塞页面中任何 JavaScript 代码的执行,甚至阻塞 UI 线程,并且用户无法单击浏览、滚动页面等。
JavaScript 中几乎所有的 I/O 基元都是非阻塞的。 网络请求、文件系统操作等。 被阻塞是个异常,这就是 JavaScript 如此之多基于回调(最近越来越多基于 promise
和 async/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
模块进行进程的创建、通信、销毁。