JS事件循环
过程
事件循环有2个重要的概念: 调用栈(先进后出)和任务队列(先进先出)。
以webworker、fs.readFile、或者setTimeout为例,此时会进入到异步任务中,完成任务后会发送任务(就是回调函数)到任务队列;然后主线程当调用栈执行完之后,会取任务(回调函数)放到调用栈来执行,不断重复(loop)直至清空调用栈。
如何知晓主线程调用栈是否为空?
js引擎的monitoring process进程不断检查
栈和任务(回调函数)队列的关系
调用栈、执行上下文与作用域
在js调用方法的时候会生成方法对应的执行环境-也就是执行上下文,这个环境保存着私有作用域和上层作用域:
函数内变量、this对象、上层作用域的变量、函数参数; 调用栈中就会保存每个方法的执行环境。
浏览器的事件循环
另外,任务队列基于类型分为宏任务和微任务。
宏任务类型: script标签、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
微任务类型:Promise、MutaionObserver、process.nextTick(Node.js 环境); async/await
执行过程:
特别需要注意的是: 执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行
取一个宏任务,执行完毕后取出微任务队列的所有微任务按顺序全部执行完;重复直至2个队列都清空。
例子:
<body>
<script>
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
})
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
new Promise((resolve) => {
console.log('我是在微任务执行过程中产生的新微任务 new')
resolve()
}).then(() => {
console.log('我是在微任务执行过程中产生的新微任务 then')
})
})
console.log('同步代码3');
</script>
<script>
console.log('同步代码4');
new Promise((resolve) => {
console.log('同步代码5')
resolve()
}).then(() => {
console.log('promise.then 2')
})
</script>
</body>
执行过程:
- 第一个script宏任务 输出同步代码1、同步代码2、同步代码3
- promise.then ; 我是在微任务执行过程中产生的新微任务 new ; 然后此时生成了新的微任务会立即执行, 输出 我是在微任务执行过程中产生的新微任务 then;
- 第2个script宏任务 同步代码4 同步代码5
- 第2个script宏任务中的微任务 promise.then 2
- setTimeout宏任务 setTimeout
这个case中 setTimeout会在script标签后再执行
例子2: 看这个
Node.js与浏览器事件循环的不同
在Node.js中,除了正常同步的代码执行,还有许多异步的api都是关于文件系统、网络,而这些实现都是在libuv进行,它是一个异步IO 库,负责文件和网络的io。
IO api 的 3 种形式:
原生支持promise api的只有DNS模块和fs模块;如果其他api也想用promise风格调用,可以先用Node.js的util.promisify方法包裹一下
基于promise
import { unlink } from 'node:fs/promises';
try {
await unlink('/tmp/hello');
console.log('successfully deleted /tmp/hello');
} catch (error) {
console.error('there was an error:', error.message);
}
基于回调
import { unlink } from 'node:fs';
unlink('/tmp/hello', (err) => {
if (err) throw err;
console.log('successfully deleted /tmp/hello');
});
基于同步执行
import { unlinkSync } from 'node:fs';
try {
unlinkSync('/tmp/hello');
console.log('successfully deleted /tmp/hello');
} catch (err) {
// handle the error
}
JS的多进程与多线程
虽然js执行过程是单线程的,但是目前不管是浏览器还是Node.js我们都有对应的API来调用多线程或者多进程。
浏览器中的多线程
Web Workers: 在浏览器中,我们可以使用Web Workers来做一些CPU密集型的事情, 比如图像计算、服务器轮训、或者预加载图片
// 主线程代码
const worker = new Worker('worker.js');
worker.onmessage = function (e) {
console.log('Received message from worker:', e.data);
};
worker.postMessage('Hello from the main thread!');
// worker.js
self.onmessage = function (e) {
console.log('Received message in worker:', e.data);
self.postMessage('Hello from the worker thread!');
};
WebAssembly 可以在浏览器中执行多线程的原生二进制代码
WebAssembly.instantiateStreaming(fetch("fail.wasm"))
.then(obj => {
obj.instance.exports.fail_me();
});
Node.js中的多进程
child_process Node.js通过child_process
模块支持多进程编程:
const { fork } = require('child_process');
const childProcess = fork('child.js');
childProcess.on('message', (message) => {
console.log('Message from child process:', message);
});
childProcess.send('Hello from the parent process!');
// child.js
process.on('message', (message) => {
console.log('Message from parent process:', message);
process.send('Hello from the child process!');
});
使用cluster
模块基于cpu核数创建多个子进程
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// 如果是主进程,创建子进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`子进程 ${worker.process.pid} 已退出`);
});
} else {
// 如果是子进程,创建 HTTP 服务器
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, World!\n');
}).listen(8000);
console.log(`子进程 ${process.pid} 已启动`);
}
相关文章
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Event_loop MDN 并发模型与事件循环
https://tech.meituan.com/2018/08/23/deep-understanding-of-jscore.html 深入理解JSCore
https://juejin.cn/post/6844903512845860872#comment 这一次,彻底弄懂 JavaScript 执行机制
https://zhuanlan.zhihu.com/p/33058983 详解JavaScript中的Event Loop(事件循环)机制
https://blog.51cto.com/u_15506823/5132257 Node.js 异步 api 的本质和 libuv