本文作者:IMWeb sugerpocket 原文出处:IMWeb社区 未经同意,禁止转载
众所周知,javascript是单线程的,其通过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。
浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。
实际上,js 引擎并不只维护一个任务队列,总共有两种任务
setTimeout
, setInterval
, setImmediate
,I/O
, UI rendering
Promise
, process.nextTick
, Object.observe
, MutationObserver
, MutaionObserver
那么两种任务的行为有何不同呢?
实验一下,请看下段代码
setTimeout(function() {
console.log(4);
}, 0);
var promise = new Promise(function executor(resolve) {
console.log(1);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
输出:
1 2 3 5 4
这说明 Promise.then
注册的任务先执行了。
我们再来看一下之前说的 Promise
注册的任务属于microTask
,setTimeout
属于 Task,两者有何差别?
实际上,microTasks
和 Tasks
并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:
也就是说,microTasks 队列在一次事件循环里面不止检查一次,我们做个实验
// 添加三个 Task
// Task 1
setTimeout(function() {
console.log(4);
}, 0);
// Task 2
setTimeout(function() {
console.log(6);
// 添加 microTask
promise.then(function() {
console.log(8);
});
}, 0);
// Task 3
setTimeout(function() {
console.log(7);
}, 0);
var promise = new Promise(function executor(resolve) {
console.log(1);
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3);
输出为
1 2 3 5 4 6 8 7
microTasks
会在每个 Task
执行完毕之后检查清空,而这次 event-loop
的新 task
会在下次 event-loop
检测。
实际上,node.js环境下,异步的实现根据操作系统的不同而有所差异。而不同的异步方式处理肯定也是不相同的,其并没有严格按照js单线程的原则,运行环境有可能会通过其他线程完成异步,当然,js引擎还是单线程的。
node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js将事件驱动的I/O模型与适合该模型的编程语言(Javascript)融合在了一起。随着node.js的日益流行,node.js需要同时支持windows, 但是libev只能在Unix环境下运行。Windows 平台上与kqueue(FreeBSD)或者(e)poll(Linux)等内核事件通知相应的机制是IOCP。libuv提供了一个跨平台的抽象,由平台决定使用libev或IOCP。
关于event loop,node.js 环境下与浏览器环境有着巨大差异。
先来一张图
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
I/O callbacks 这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。
poll 阶段的功能有两个
如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况
这个阶段在 poll 结束后立即执行,setImmediate 的回调会在这里执行。
一般来说,event loop 肯定会进入 poll 阶段,当没有 poll 任务时,会等待新的任务出现,但如果设定了 setImmediate,会直接执行进入下个阶段而不是继续等。
close 事件在这里触发,否则将通过 process.nextTick 触发。
var fs = require('fs');
function someAsyncOperation (callback) {
// 假设这个任务要消耗 95ms
fs.readFile('/path/to/file', callback);
}
var timeoutScheduled = Date.now();
setTimeout(function () {
var delay = Date.now() - timeoutScheduled;
console.log(delay + "ms have passed since I was scheduled");
}, 100);
// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {
var startCallback = Date.now();
// 消耗 10ms...
while (Date.now() - startCallback < 10) {
; // do nothing
}
});
当event loop进入 poll 阶段,它有个空队列(fs.readFile()尚未结束)。所以它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,然后它的回调被加到 poll 的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的 下限时间,然后回到 timers 阶段,执行timer的回调。
所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
现在我们应该知道两者的不同,他们的执行阶段不同,setImmediate() 在 check 阶段,而settimeout 在 poll 阶段执行。但,还不够。来看一下例子。
// timeout_vs_immediate.js
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
结果居然是不确定的,why?
还是直接给出解释吧。
那我们再来一个
// timeout_vs_immediate.js
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
输出始终为
$ node timeout_vs_immediate.js
immediate
timeout
这个就很好解释了吧。 fs.readFile 的回调执行是在 poll 阶段。当 fs.readFile 回调执行完毕之后,会直接到 check 阶段,先执行 setImmediate 的回调。
nextTick 比较特殊,它有自己的队列,并且,独立于event loop。 它的执行也非常特殊,无论 event loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。
https://juejin.im/entry/58332d560ce46300610e4bad https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ https://flyyang.github.io/2017/03/07/javascript中的microtask与task/ https://hao5743.github.io/2017/02/27/对node事件循环机制中Macrotask和Microtask的理解/ https://github.com/ccforward/cc/issues/48 https://github.com/creeperyang/blog/issues/21 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop