You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
varfs=require('fs');functionsomeAsyncOperation(callback){// 假设这个任务要消耗 95msfs.readFile('/path/to/file',callback);}vartimeoutScheduled=Date.now();setTimeout(function(){vardelay=Date.now()-timeoutScheduled;console.log(delay+"ms have passed since I was scheduled");},100);// someAsyncOperation要消耗 95 ms 才能完成someAsyncOperation(function(){varstartCallback=Date.now();// 消耗 10ms...while(Date.now()-startCallback<10){;// do nothing}});
众所周知,javascript 是单线程的,其通过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。
浏览器环境
浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。
实际上,js 引擎并不只维护一个任务队列,总共有两种任务
setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
Promise
,process.nextTick
,Object.observe
,MutationObserver
,MutaionObserver
那么两种任务的行为有何不同呢?
实验一下,请看下段代码
输出:
这说明
Promise.then
注册的任务先执行了。我们再来看一下之前说的
Promise
注册的任务属于microTask
,setTimeout
属于 Task,两者有何差别?实际上,
microTasks
和Tasks
并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:也就是说,microTasks 队列在一次事件循环里面不止检查一次,我们做个实验
输出为
microTasks
会在每个Task
执行完毕之后检查清空,而这次event-loop
的新task
会在下次event-loop
检测。Node 环境
实际上,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
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
I/O callbacks
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。
poll
poll 阶段的功能有两个
如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况
如果设定了 setImmediate 回调,会直接跳到 check 阶段。
如果没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并立即执行。
check
这个阶段在 poll 结束后立即执行,setImmediate 的回调会在这里执行。
一般来说,event loop 肯定会进入 poll 阶段,当没有 poll 任务时,会等待新的任务出现,但如果设定了 setImmediate,会直接执行进入下个阶段而不是继续等。
close
close 事件在这里触发,否则将通过 process.nextTick 触发。
一个例子
当event loop进入 poll 阶段,它有个空队列(fs.readFile()尚未结束)。所以它会等待剩下的毫秒,
直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,然后它的回调被加到 poll
的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的
下限时间,然后回到 timers 阶段,执行timer的回调。
所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
setImmediate() vs setTimeout()
现在我们应该知道两者的不同,他们的执行阶段不同,setImmediate() 在 check 阶段,而settimeout 在 poll 阶段执行。但,还不够。来看一下例子。
结果居然是不确定的,why?
还是直接给出解释吧。
那我们再来一个
输出始终为
这个就很好解释了吧。
fs.readFile 的回调执行是在 poll 阶段。当 fs.readFile 回调执行完毕之后,会直接到 check 阶段,先执行 setImmediate 的回调。
process.nextTick()
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%E4%B8%AD%E7%9A%84microtask%E4%B8%8Etask/
https://hao5743.github.io/2017/02/27/%E5%AF%B9node%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E6%9C%BA%E5%88%B6%E4%B8%ADMacrotask%E5%92%8CMicrotask%E7%9A%84%E7%90%86%E8%A7%A3/
ccforward/cc#48
creeperyang/blog#21
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
The text was updated successfully, but these errors were encountered: