Open creeperyang opened 7 years ago
@creeperyang Hi,我想说的是那个105ms的例子,我用js在浏览器当中执行异步代码的原理也能解释的通,那么问题是不是WebAPIs是浏览器中的,在Nodejs中就是libuv,EventLoop只是等待堆栈执行完才把TaskQueue的回调Push到堆栈执行,按照我的理解是,someAsyncOperation方法先完成了,他被push到任务队列中,然后EventLoop判断Stack中空了,执行那剩下10秒,但是过了5s后,Settimeout回调任务完成了,被Push到任务队列中,但是当前Stack正在执行后面那5s不为空,所以EventLoop要等到为空才执行这个回调,所以是105ms。 js在web中的例子:http://latentflip.com/loupe/?code=JC5vbignYnV0dG9uJywgJ2NsaWNrJywgZnVuY3Rpb24gb25DbGljaygpIHsKICAgIHNldFRpbWVvdXQoZnVuY3Rpb24gdGltZXIoKSB7CiAgICAgICAgY29uc29sZS5sb2coJ1lvdSBjbGlja2VkIHRoZSBidXR0b24hJyk7ICAgIAogICAgfSwgMjAwMCk7Cn0pOwoKY29uc29sZS5sb2coIkhpISIpOwoKc2V0VGltZW91dChmdW5jdGlvbiB0aW1lb3V0KCkgewogICAgY29uc29sZS5sb2coIkNsaWNrIHRoZSBidXR0b24hIik7Cn0sIDUwMDApOwoKY29uc29sZS5sb2coIldlbGNvbWUgdG8gbG91cGUuIik7!!!PGJ1dHRvbj5DbGljayBtZSE8L2J1dHRvbj4%3D
@zColdWater 你理解得应该没什么问题,就是表述有点不是很好懂(比如没有event loop为空的说法)。
下面总结几点关于event loop的:
process.nextTick
和Promise
都是microtask,即在进入下个event loop前执行(也可以理解成不是event loop相关的概念)。setTimeout/setImmediate/io
是task,会把回调延迟到后面的某个event loop执行。那在H5新特征的多线程JS处理代码中,这个异步该如何处理呢?
@creeperyang 谢谢,🍻。
@chn777 你是指web worker吗?web worker工作在单独的线程,有自己的event loop。
@creeperyang 是的是的,谢谢了
Hi, 不是很理解poll阶段具体是负责什么业务。 首先,按照我对文章的理解,timers的回调执行是在timers阶段,不过判断却是在poll阶段,一旦条件满足就返回timers阶段,如果这样把timers放在poll后面才合适; 其次,时间回调和IO回调都有相对应的处理阶段,那么poll阶段要处理的回调是什么回调呢。或者,poll不是执行回调,而只是遍历所有的IO,将满足条件的回调填充到timer和IO的队列里去。这么来讲,poll队列对应的事件呢
@ChuanTS
首先I/O callbacks
这个阶段,名字有点误导,并不是所有I/O的回调都在这里执行,官网上举的例子是
For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the I/O callbacks phase
而fs.readFile
的回调是在poll
执行的,我的理解是fs.readFile
调用了操作系统的API,文件读完之后通过event
的方式通知V8,而event的处理是在`poll阶段
readStream
的data
,这些api的回调应该都在poll
阶段完成您好,现在才看到您这篇文章,想请问以下代码中:
setImmediate(function(){
console.log("setImmediate");
setImmediate(function(){
console.log("嵌套setImmediate");
});
process.nextTick(function(){
console.log("nextTick");
})
});
执行结果:
setImmediate
nextTick
嵌套setImmediate
我在setImmediate中又调用了一个setImmediate,称外层setImmediate为A,内层setImmediate为B,那么我想问,为什么此时B不会紧接着A执行,而是使得其中的nextTick先执行呢?我认为是B应该也被加入了setImmediate所在的队列,应该会继续执行啊,希望能得到您的回复,谢谢!
@Julyrainy 简单说,setImmediate
只有在 check
阶段执行,nextTick
在每个阶段的末尾都会执行。即,nextTick
属于 microtask,而setImmediate
属于macrotask,希望这可以帮助你理解。
可以联系 #21 一起看。
@creeperyang 多谢您的回复,主要就是macrotask和microtask我不理解。
setImmediate 只有在 check 阶段执行
我的困惑就在于此,在我上方例子中,A执行的时候必然是处于check阶段,A执行时候新注册了一个setImmediate,也就是B,此时B为何不能继续执行?是因为A执行之后直接退出了check阶段吗?
另外看过好多资料说macrotask一次tick只能执行一个,macrotask队列中的下一个task得等到下次tick才能执行,而microtask能执行多次,这是正确的说法吗?
@Julyrainy https://nodejs.org/api/timers.html#timers_setimmediate_callback_args
Schedules the "immediate" execution of the callback after I/O events' callbacks. Returns an Immediate for use with clearImmediate().
When multiple calls to setImmediate() are made, the callback functions are queued for execution in the order in which they are created. The entire callback queue is processed every event loop iteration. If an immediate timer is queued from inside an executing callback, that timer will not be triggered until the next event loop iteration.
文档中很清楚地写明了:
setImmediate()
则把回调都放入队列,在 check 阶段都会执行;setImmediate()
回调里调用setImmediate()
,则放到下次 event loop。其实这很好理解,一个 event loop 有多个阶段,每个阶段做对应的事,timer 阶段会把需要执行的 回调 (队列)都执行,check 阶段也会把 把需要执行的 回调 (队列)都执行。
一个帮助理解的例子:
const async_hooks = require('async_hooks')
const fs = require('fs')
let indent = 0
async_hooks.createHook({
init(asyncId, type, triggerId) {
const cId = async_hooks.currentId()
print(`${getIndent(indent)}${type}(${asyncId}): trigger: ${triggerId} scope: ${cId}`)
},
before(asyncId) {
print(`${getIndent(indent)}before: ${asyncId}`)
indent += 2
},
after(asyncId) {
indent -= 2
print(`${getIndent(indent)}after: ${asyncId}`)
},
destroy(asyncId) {
print(`${getIndent(indent)}destroy: ${asyncId}`)
},
}).enable()
function print(str) {
fs.writeSync(1, str + '\n');
}
function getIndent(n) {
return ' '.repeat(n)
}
print('start')
setTimeout(() => {
print('--outter: timeout1')
setImmediate(() => {
print('--inner: setImmediate1')
process.nextTick(() => {
print('--inner-inner: nextTick')
})
})
setTimeout(() => {
print('--inner: setTimeout')
})
setImmediate(() => {
print('--inner: setImmediate2')
})
process.nextTick(() => {
print('--inner: nextTick')
})
})
process.nextTick(() => {
print('--outter: nextTick')
})
setTimeout(() => {
print('--outter: timeout2')
})
print('end')
借助 async_hooks 模块,可以看到以下输出:
//// loop 1
start
Timeout(2): trigger: 1 scope: 1
TIMERWRAP(3): trigger: 1 scope: 1
TickObject(4): trigger: 1 scope: 1
Timeout(5): trigger: 1 scope: 1
end
before: 4
--outter: nextTick
after: 4
///// loop 2
before: 3
before: 2
--outter: timeout1
Immediate(6): trigger: 2 scope: 2
Timeout(7): trigger: 2 scope: 2
Immediate(8): trigger: 2 scope: 2
TickObject(9): trigger: 2 scope: 2
after: 2
before: 5
--outter: timeout2
after: 5
after: 3
before: 9
--inner: nextTick
after: 9
destroy: 4
destroy: 2
destroy: 5
destroy: 9
///// loop 3
before: 3
before: 7
--inner: setTimeout
after: 7
after: 3
destroy: 7
before: 6
--inner: setImmediate1
TickObject(10): trigger: 6 scope: 6
after: 6
before: 8
--inner: setImmediate2
after: 8
before: 10
--inner-inner: nextTick
after: 10
destroy: 6
destroy: 8
destroy: 10
destroy: 3
很容易看出,两个 timeout 回调在同一个 loop 执行,两个 setImmediate 同一个 loop 执行。
当我们去除 async_hooks 模块,重复跑这段代码,有时可能会有不同的输出:
start
end
--outter: nextTick
--outter: timeout1
--inner: nextTick
--outter: timeout2
--inner: setTimeout
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick
或:
start
end
--outter: nextTick
--outter: timeout1
--outter: timeout2
--inner: nextTick
--inner: setImmediate1
--inner: setImmediate2
--inner-inner: nextTick
--inner: setTimeout
我们知道 nextTick
是 microtask,那么 nextTick
出现在不同位置,告诉我们这几次运行中 event loop 的次数和划分和之前有所不同。
之所以出现这样的情况,应该跟 timer 的时间判断有关。setTimeout(fn)
实质上即 setTimeout(fn, 1)
,所以在运行环境稍有差异时,可能会有超出 1ms 的运行时间不同,timer 的时间判断也随即不一样。
@creeperyang 万分感谢您细心的回复,我有点清晰了,我在好好研读一下
@Julyrainy 一次事件循环只执行一次宏任务(task), 然后执行多个微任务(microtask). 这个是浏览器端的事件循环模型, 不适用于 nodejs. 我之前也在关于 nodejs 的事件循环看到这种说法, 这个是错误的. 关于浏览器端的事件循环可以参考 https://html.spec.whatwg.org/multipage/webappapis.html#event-loops . 如果楼主有兴趣的话, 不妨也翻译下.
@creeperyang 我看到nextTick的这种用法:
var http = require('http');
function compute() { // 执行一个cpu密集的任务 // ... process.nextTick(compute); }
http.createServer(function(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World'); }).listen(5000, '127.0.0.1');
compute();
请教一下,这里的nextTick嵌套,是不是在每个“阶段”执行一次?否则岂不是死循环了?
Looking back at our diagram, any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues. This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
说的是不是这种情况?
@mygaochunming 是。但不是每个“阶段”执行一次,而是在该阶段持续执行 nextTick 注册的回调。
@creeperyang 我上面那段代码摘自http://www.cnblogs.com/lengyuhong/archive/2013/03/31/2987745.html 二. cpu高密集代码段
这里岂不是死循环,即文章中说的“假死”。
@mygaochunming 你贴的代码的确是死循环,compute
会重复执行。
macrotasks: setTimeout ,setInterval, setImmediate,requestAnimationFrame,I/O ,UI渲染 microtasks: Promise, process.nextTick, Object.observe, MutationObserver
当一个程序有:setTimeout, setInterval ,setImmediate, I/O, UI渲染,Promise ,process.nextTick, Object.observe, MutationObserver的时候:
1.先执行 macrotasks:I/O -》 UI渲染-》requestAnimationFrame
2.再执行 microtasks :process.nextTick -》 Promise -》MutationObserver ->Object.observe
3.再把setTimeout setInterval setImmediate【三个货不讨喜】 塞入一个新的macrotasks,依次:setTimeout ,setInterval --》setImmediate
setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);
结果是:3 4 6 8 7 5 2 1
const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
输出
immediate
readfile
能解释一下是为什么么?
我觉得代码过1s执行完后,事件放入poll阶段的队列,应该首先执行fs.readfile()
回调,之后再执行check阶段的setImmediate
回调。
Hi @Zerxoi
感謝你提供了這麼棒的一個範例,這個範例可以幫助我們更了解 libuv 的 event loop 不過我自己好奇嘗試了一下,發現 stackoverflow 上的這個解釋並不正確
我在 libuv event loop 代碼插入了一堆 printf 觀察了一下,大概至上是改這樣:
int count = 0;
while (r != 0 && loop->stop_flag == 0) {
count++;
printf("\n\n===== LOOP ROUND %d =====\n", count);
uv__update_time(loop);
printf("[uv__run_timers]: enter\n");
uv__run_timers(loop);
printf("[uv__run_timers]: exit\n");
printf("[uv__run_pending]: enter\n");
ran_pending = uv__run_pending(loop);
printf("[uv__run_pending]: exit\n");
printf("[uv__run_idle]: enter\n");
uv__run_idle(loop);
printf("[uv__run_idle]: exit\n");
printf("[uv__run_prepare]: enter\n");
uv__run_prepare(loop);
printf("[uv__run_prepare]: exit\n");
...
此外,因為 uv__io_poll (kqueue.c) 的實作也很長,我在裡面一些 if else 的分支部分也插入 printf ,像是這樣
if (ev->filter == EVFILT_READ) {
printf("[uv__io_poll]: ev->filter == EVFILT_READ\n");
...
}
if (ev->filter == EV_OOBAND) {
printf("[uv__io_poll]: ev->filter == EV_OOBAND\n");
...
}
if (ev->filter == EVFILT_WRITE) {
printf("[uv__io_poll]: ev->filter == EVFILT_WRITE\n");
...
}
然後編譯 node,執行你給的範例
const fs = require('fs')
const now = Date.now();
setTimeout(() => console.log('timer'), 10);
fs.readFile(__filename, () => console.log('readfile'));
setImmediate(() => console.log('immediate'));
while(Date.now() - now < 1000) {
}
得到的結果是這樣子
===== LOOP ROUND 1 =====
[uv__run_timers]: enter
[uv__run_timers]: handle->timer_cb
timer
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
immediate
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit
===== LOOP ROUND 2 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit
===== LOOP ROUND 3 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit
===== LOOP ROUND 4 =====
[uv__run_timers]: enter
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: ev->filter == EVFILT_READ
[uv__io_poll]: w != &loop->signal_io_watcher
[uv__io_poll]: w->cb(loop, w, revents)
readfile
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit
若將 readFile 拿掉:
const fs = require("fs");
const now = Date.now();
setTimeout(() => console.log("timer"), 10);
setImmediate(() => console.log("immediate"));
while (Date.now() - now < 1000) {}
則會得到
===== LOOP ROUND 1 =====
[uv__run_timers]: enter
[uv__run_timers]: handle->timer_cb
timer
[uv__run_timers]: exit
[uv__run_pending]: enter
[uv__run_pending]: w->cb(loop, w, POLLOUT)
[uv__run_pending]: exit
[uv__run_idle]: enter
[uv__run_##name]: h->name##_cb(h)
[uv__run_idle]: exit
[uv__run_prepare]: enter
[uv__run_prepare]: exit
[uv__io_poll]: enter
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: (w->events & POLLIN) == 0 && (w->pevents & POLLIN) != 0
[uv__io_poll]: exit
[uv__run_check]: enter
[uv__run_##name]: h->name##_cb(h)
immediate
[uv__run_check]: exit
[uv__run_closing_handles]: enter
[uv__run_closing_handles]: exit
可以確定的是 readFile 的 callback 確實是在 uvio_poll 這個階段執行的,不是 uv__run_pending 而 LOOP ROUND 1~4 裡面的 uvio_poll 都有在做事情看起來應該是 readFile 產生的 event 沒辦法在一個 LOOP ROUND 裡面做完
@rueian 👍
提供的这段 log 对了解 libuv 的处理流程很有帮助!
据说 requestAnimationFrame 不算是 macro task。
https://stackoverflow.com/questions/43050448/when-will-requestanimationframe-be-executed
Two questions 1 From this article Understand Node.JS eventloop
Misconception 1: The event loop runs in a separate thread than the user code Misconception There is a main thread where the JavaScript code of the user (userland code) runs in and another one that runs the event loop. Every time an asynchronous operation takes place, the main thread will hand over the work to the event loop thread and once it is done, the event loop thread will ping the main thread to execute a callback.
Reality There is only one thread that executes JavaScript code and this is the thread where the event loop is running. The execution of callbacks (know that every userland code in a running Node.js application is a callback) is done by the event loop. We will cover that in depth a bit later.
So it seems there is only one thread in Node.js, do I understand right?
2 文中提到 ‘I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。’ 。 但是原文中的意思是這個階段只是執行系統圖回調,你所説的執行所有回調是什麽意思呢?能都詳細説明,謝謝。
@LeonAppDev Q1: JS是单线程的,对浏览器和Node.js都成立。这里的单线程的确切意思是JS的执行只在单一线程上,但Node.js 和 浏览器本身都是多线程甚至多进程的。
Q2: 我看了下https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/的最新文档似乎有更新,我从头再看一遍来给一个确切的回答。
关于Q2,最新的文档似乎跟我原来看的略有不同,根据最新的文档,有些措辞需要更新,我会更新上面的相关部分,并在这里重点讲一下 pending(I/O) callbacks phase vs poll phase :
setImmediate
添加的回调,event loop会在这里等待I/O callbacks被添加到poll queue,并立即执行。大部分的I/O回调会在poll阶段被执行,但某些系统操作(比如TCP类型错误)执行回调会安排在pending callbacks阶段。
更多细节在http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop。
順帶一提 Node.js 10 開始有可以利用多線程的 Worker Thread 功能 https://nodejs.org/api/worker_threads.html
@creeperyang 非常感謝及時回复,那么关于Q2我还有一個问题,为什么系统操作的回調不会在poll阶段來执行呢?我的理解是Poll阶段只执行用户定义和Node库定义的回調,也就是比較高層的回調,而把底層的回調放在Pending Callbacks階段執行,那麽這麽做有什麽好處呢?是不是爲了優先回復Success的用戶request,而把failure的response放到後面來執行從而提高用戶的體驗?這是我的理解。
@LeonAppDev 更准确的说,pending callbacks
阶段执行的是一些被有意延迟的回调。
详情可以参考Node.js更新相应文档的原因:
Only some callbacks that were deliberately delayed will be executed in this phase (hence "pending").
下面讲一讲ECONNREFUSED
的回调(处理错误)被延迟到pending callbaks的可能原因。
int uv__tcp_connect(uv_connect_t* req,
uv_tcp_t* handle,
const struct sockaddr* addr,
unsigned int addrlen,
uv_connect_cb cb) {
int err;
int r;
assert(handle->type == UV_TCP);
if (handle->connect_req != NULL)
return -EALREADY; /* FIXME(bnoordhuis) -EINVAL or maybe -EBUSY. */
err = maybe_new_socket(handle,
addr->sa_family,
UV_STREAM_READABLE | UV_STREAM_WRITABLE);
if (err)
return err;
handle->delayed_error = 0;
do {
errno = 0;
r = connect(uv__stream_fd(handle), addr, addrlen);
} while (r == -1 && errno == EINTR);
/* We not only check the return value, but also check the errno != 0.
* Because in rare cases connect() will return -1 but the errno
* is 0 (for example, on Android 4.3, OnePlus phone A0001_12_150227)
* and actually the tcp three-way handshake is completed.
*/
if (r == -1 && errno != 0) {
if (errno == EINPROGRESS)
; /* not an error */
else if (errno == ECONNREFUSED)
/* If we get a ECONNREFUSED wait until the next tick to report the
* error. Solaris wants to report immediately--other unixes want to
* wait.
*/
handle->delayed_error = -errno;
else
return -errno;
}
uv__req_init(handle->loop, req, UV_CONNECT);
req->cb = cb;
req->handle = (uv_stream_t*) handle;
QUEUE_INIT(&req->queue);
handle->connect_req = req;
uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);
if (handle->delayed_error)
uv__io_feed(handle->loop, &handle->io_watcher);
return 0;
}
从libuv源码可以看到,对ECONNREFUSED
错误,Solaris系统会立即报告错误,而其它unixes操作系统则会等待。所以,统一塞到pending queue里面,延迟报告错误是一个很自然的选择——屏蔽系统差异,保持一致性。
赞,关注一波
问题1:
看到有回复得出readFile的回调在 I/O callbacks 触发,按照这样的话,我觉得流程是这样:
但这样的话完全不符合实际结果,到底是什么原因?
const fs = require('fs')
fs.readFile(__filename, () => {
console.log('readFile')
setTimeout(() => {
console.log('readFile timeout')
}, 0)
setImmediate(() => {
console.log('readFile immediate')
})
fs.readFile(__filename, () => {
console.log('readFile readFile')
})
})
setImmediate(() => {
console.log('immediate')
})
setTimeout(() => {
console.log('timeout')
}, 0)
结果是
immediate
timeout
readFile
readFile immediate
readFile timeout
readFile readFile
问题2: 下面的代码不在主模块调用,也不在I/O操作中调用,为什么结果会不一样?
setImmediate里面嵌套
setImmediate(() => {
console.log('setImmediate')
setTimeout(() => {
console.log('setImmediate 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate 里面的 setImmediate')
})
});
// 结果
// setImmediate
// setImmediate 里面的 setTimeout
// setImmediate 里面的 setImmediate
// 或
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout
setTimeout里面嵌套
setTimeout(() => {
console.log('setImmediate')
setTimeout(() => {
console.log('setImmediate 里面的 setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate 里面的 setImmediate')
})
}, 0);
// 结果
// setImmediate
// setImmediate 里面的 setImmediate
// setImmediate 里面的 setTimeout
@surahe 刚好发现一篇文章, 解释的很清楚 https://segmentfault.com/a/1190000013102056#articleHeader9
本文是对Node.js官方文档The Node.js Event Loop, Timers, and
process.nextTick()
的翻译和理解。文章并不是一字一句严格对应原文,其中会夹杂其它相关资料,以及相应的理解和扩展。相关资料:
什么是事件循环(
Event loop
)?Event loop是什么?
WIKI定义:
Event loop是一种程序结构,是实现异步的一种机制。Event loop可以简单理解为:
所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后主线程继续执行后续的任务。
一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
主线程不断重复上面的第三步。
对JavaScript而言,Javascript引擎/虚拟机(如V8)之外,JavaScript的运行环境(runtime,如浏览器,node)维护了任务队列,每当JS执行异步操作时,运行环境把异步任务放入任务队列。当执行引擎的线程执行完毕(空闲)时,运行环境就会把任务队列里的(执行完的)任务(的数据和回调函数)交给引擎继续执行,这个过程是一个不断循环的过程,称为事件循环。
注意:JavaScript(引擎)是单线程的,Event loop并不属于JavaScript本身,但JavaScript的运行环境是多线程/多进程的,运行环境实现了Event loop。
另外,视频What the heck is the event loop anyway 站在前端的角度,用动画的形式描述了上述过程,可以便于理解。
解释Node.js的Event loop
当Node.js启动时,它会初始化event loop,处理提供的代码(代码里可能会有异步API调用,timer,以及
process.nextTick()
),然后开始处理event loop。下面是node启动的部分相关代码:
Event Loop的执行顺序
下面的示意图展示了一个简化的event loop的操作顺序:
(图来自Node.js API)
图中每个“盒子”都是event loop执行的一个阶段(phase)。
每个阶段都有一个FIFO的回调队列(queue)要执行。而每个阶段有自己的特殊之处,简单说,就是当event loop进入某个阶段后,会执行该阶段特定的(任意)操作,然后才会执行这个阶段的队列里的回调。当队列被执行完,或者执行的回调数量达到上限后,event loop会进入下个阶段。
Phases Overview 阶段总览
setTimeout()
和setInterval()
设定的回调。close
回调,timer的回调,和setImmediate()
的回调。setImmediate()
设定的回调。socket.on('close', ...)
的回调。Phases in Detail 阶段详情
timers
一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
注意:这个下限时间有个范围:
[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为1。I/O callbacks
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到
ECONNREFUSED
, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。poll
poll 阶段有两个主要功能:
当event loop进入 poll 阶段,并且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:
如果 poll 队列为空,则发生以下两件事之一:
setImmediate()
设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。setImmediate()
设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):
check
这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被
setImmediate()
设定的回调,event loop会转到 check 阶段而不是继续等待。setImmediate()
实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv
的API 来设定在 poll 阶段结束后立即执行回调。通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被
setImmediate()
设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。close callbacks
如果一个 socket 或 handle 被突然关掉(比如
socket.destroy()
),close事件将在这个阶段被触发,否则将通过process.nextTick()
触发。event loop的一个例子讲述
当event loop进入 poll 阶段,它有个空队列(
fs.readFile()
尚未结束)。所以它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()
首先结束了,然后它的回调被加到 poll 的队列并执行——这个回调耗时10ms。之后由于没有其它回调在队列里,所以event loop会查看最近达到的timer的 下限时间,然后回到 timers 阶段,执行timer的回调。所以在示例里,回调被设定 和 回调执行间的间隔是105ms。
setImmediate()
vssetTimeout()
setImmediate()
和setTimeout()
是相似的,区别在于什么时候执行回调:setImmediate()
被设计在 poll 阶段结束后立即执行回调;setTimeout()
被设计在指定下限时间到达后执行回调。下面看一个例子:
代码的输出结果是:
是的,你没有看错,输出结果是 不确定 的!
从直觉上来说,
setImmediate()
的回调应该先执行,但为什么结果随机呢?再看一个例子:
结果是:
很好,
setImmediate
在这里永远先执行!所以,结论是:
setImmediate
的回调永远先执行。那么又是为什么呢?
看
int uv_run(uv_loop_t* loop, uv_run_mode mode)
源码(deps/uv/src/unix/core.c#332):上面的代码看起来很清晰,一一对应了我们的几个阶段。
setTimeout(fn, 0)
等价于setTimeout(fn, 1)
),那么setTimeout
的回调会首先执行。setImmediate
的回调会先执行。fs.readFile
回调里设置的,setImmediate
始终先执行?因为fs.readFile
的回调执行是在 poll 阶段,所以,接下来的 check 阶段会先执行setImmediate
的回调。UV_RUN_ONCE
模式下,event loop会在开始和结束都去执行timer。理解
process.nextTick()
直到现在,我们才开始解释
process.nextTick()
。因为从技术上来说,它并不是event loop的一部分。相反的,process.nextTick()
会把回调塞入nextTickQueue
,nextTickQueue
将在当前操作完成后处理,不管目前处于event loop的哪个阶段。看看我们最初给的示意图,
process.nextTick()
不管在任何时候调用,都会在所处的这个阶段最后,在event loop进入下个阶段前,处理完所有nextTickQueue
里的回调。process.nextTick()
vssetImmediate()
两者看起来也类似,区别如下:
process.nextTick()
立即在本阶段执行回调;setImmediate()
只能在 check 阶段执行回调。