众所周知,javascript 是单线程的,其经过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。java
浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。node
实际上,js 引擎并不仅维护一个任务队列,总共有两种任务git
setTimeout
, setInterval
, setImmediate
,I/O
, UI rendering
Promise
, process.nextTick
, Object.observe
, MutationObserver
, MutaionObserver
那么两种任务的行为有何不一样呢?github
实验一下,请看下段代码web
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);
复制代码
输出:shell
1 2 3 5 4
复制代码
这说明 Promise.then
注册的任务先执行了。编程
咱们再来看一下以前说的 Promise
注册的任务属于microTask
,setTimeout
属于 Task,二者有何差异?windows
实际上,microTasks
和 Tasks
并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:promise
也就是说,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 队列。
juejin.im/entry/58332… jakearchibald.com/2015/tasks-… flyyang.github.io/2017/03/07/… hao5743.github.io/2017/02/27/… github.com/ccforward/c… github.com/creeperyang… developer.mozilla.org/zh-CN/docs/…