EventLoop 事件循环
事件轮询是一直循环往复的,只有当任务队列为空时,才会停止循环,且在每一趟循环中,每一个环节都会有对应的操作。
JavaScript 有一个主线程 main thread,和调用栈 call-stack 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。
任务队列(task queue)
Task Queue 就是承载任务的队列。JavaScript 的 Event Loop 就是会不断地过来找这个 queue,问有没有 task 可以运行运行。
同步任务(SyncTask)、异步任务(AsyncTask)
同步任务就是主线程来执行的时候立即就能执行的代码
异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行
setTimeout(()=>{
console.log(2)
});
console.log(1);
setTimeout 就是一个异步任务,主线程去执行的时候遇到 setTimeout 发现是一个异步任务,就先注册了一个异步的回调,然后接着执行下面的语句console.log(1),等上面的异步任务等待的时间到了以后,在执行console.log(2)
- 主线程自上而下执行所有代码
- 同步任务直接进入到主线程被执行,而异步任务则进入到
Event Table并注册相对应的回调函数 - 异步任务完成后,
Event Table会将这个函数移入Event Queue - 主线程任务执行完了以后,会从
Event Queue中读取任务,进入到主线程去执行。 - 循环如上
- 主线程任务执行完毕,看
Event Queue是否有待执行的 task,这里是不断地检查,只要主线程的task queue没有任务执行了,主线程就一直在这等着
宏任务(MacroTask)、微任务(MicroTask)
JavaScript 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(MacroTask)和微任务(MicroTask)。
宏任务
- 主体script
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务 Promise.then,process.nextTick
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
不同执行环境下的EventLoop
浏览器环境下的EventLoop
主线程执行栈处理
执行顺序(宏任务->微任务->宏任务->微任务···)
具体怎么执行,咱们一起看两个案例分析,看完绝对就没问题了。
setTimeout(() => {
new Promise(resolve => {
// 宏任务
console.log('1') // 1
resolve();
}).then(() => {
//微任务
console.log('3') // 3
//setTimeout(() => {
// console.log('5') // 5
//}, 1000)
});
// 宏任务
console.log(2); // 2
// 宏任务
setTimeout(() => {
console.log(4)
}, 1000); // 4
})
##
- 首先,会执行第一个setTimeout(宏任务),把其推入eventLoop,由于没有其他的宏任务,
当时间到了时,会直接将其回调推入执行栈。
- 宏任务先执行, newPromise先执行打印 1 ,
- 再打印 2,
- 然后再执行setTimeout,将其推入eventLoop,
- 没有宏任务了,再执行微任务.then(),打印 3,
- 最后主线程执行完毕,将第二个 setTimeout执行内容从事件队列中推入主线程
- 最后打印 4
setTimeout(function() {
console.log('5');
}, 3000)
setTimeout(function() {
console.log('4');
})
new Promise(function(resolve) {
console.log('1');
resolve()
}).then(function() {
console.log('3');
})
console.log('2');
// 12345
##
这段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log(),立即执行。
- 微任务中去promise.then
ok,第一轮事件循环结束了,我们开始第二轮循环,
- 当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
Node环境下的EventLoop
Node.js的运行机制如下:
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。- V8引擎再将结果返回给用户
要明确执行环境,Node 和浏览器的Event Loop是两个有明确区分的事物,不能混为一谈。nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
-
Timer, 处理所有 setTimeout 和 setInterval 的回调
对于timers中队列的处理,setTimeout或setInterval中的函数都添加到队列里,同时记下来这些函数什么时间被调用到了调用时间就调用,没到时间就进入下一阶段,然后会停留在poll阶段
-
i/o cycle
-
Pending I/O Callback, 执行 I/O 回调,文件操作、网络操作等
除了以下操作的回调函数,其他的回调函数都在这个阶段执行。
setTimeout()和setInterval()的回调函数,(因为它在timer阶段执行)setImmediate()的回调函数,(因为它在Check阶段执行)- 用于关闭请求的回调函数,比如
socket.on('close', ...),(因为它在Close callbacks阶段执行)
-
Idle, Prepare 内部使用
-
Poll
这个阶段是轮询时间,用于等待还未返回的
I/O 事件,比如服务器的回应、用户移动鼠标等等。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。执行I/O回调。在这个阶段会一直去问执行相关任务的模块任务执行完了没有,一旦任务执行完成,相关的数据就会放到一个回调里面然后加入到
poll queue中,也就是除了timers阶段外的所有回调都是在poll这个阶段处理的。poll阶段会一直重复检查刚才timers里没有到时间的计时器有没有到时间,如果到时间了,就直接通过check到达timers阶段通知timers执行这个回调,然后从timers队列中清除。
-
-
Check,只处理 setImmediate 的回调函数
-
Close Callback,专门处理一些 close 类型的回调,如关闭网络连接等
> **关于setTImeout和setImmediate的执行顺序** > 答:正常情况下是setImmediate先执行,只有第一次启动nodejs的情况下timers里的定时器到时间了`才可能`setTimeout先执行。 > 原因:因为nodejs启动会执行三件事,开始执行脚本也就是执行你页面的代码, 这时候会执行你的setTImeout,然后才会去处理event loop, 而从执行脚本到处理event loop中间也需要时间,setTimeout最小的时间是4ms, 如果从执行脚本到处理event loop花了5ms时间,那么一进入timers就会发现时间到了就会立刻处理回调, 所以这时候setTimeout就会先执行
process.nextTick()
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列 nextTickQueue,当每个阶段完成后(包括,一开始执行所有的同步任务,执行完之后会立即清空nextTickQueue以及microTaskQueue,有点类似macroTaskQueue script转到执行栈的这个过程),如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行
然后进入下一阶段,包括nodejs启动的时候也会执行。
如果存在 nextTickQueue,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Node Event Loop 样例
console.log('1');
setTimeout(function () {
console.log('5');
process.nextTick(function () {
console.log('7');
})
new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('8')
})
})
setImmediate(() => {
console.log('13');
process.nextTick(function () {
console.log('15');
})
new Promise(function (resolve) {
console.log('14');
resolve();
}).then(function () {
console.log('16')
})
});
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('2');
resolve();
}).then(function () {
console.log('4')
})
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('11');
})
new Promise(function (resolve) {
console.log('10');
resolve();
}).then(function () {
console.log('12')
})
})
// 1 2 3 4 。。。
# 解释一下
先执行一遍,主线程执行
先打印 1
注册2个setTimeout,加入timer队列
注册setImmediate,加入check队列
TicketQueue中加入回调
执行promise, 打印 2
微任务队列中加入promise.then
第一轮宏任务执行完毕,
TicketQueue先执行,打印 3
微任务再执行,打印 4
poll队列通知Timer执行,
打印 5
TickQueue中加入
执行promise, 打印 6
加入微任务
当前阶段执行完毕,
tickQueue执行, 打印 7
微任务执行,打印8
poll队列通知Timer执行,
打印 9
TickQueue中加入
执行promise, 打印 10
加入微任务
当前阶段执行完毕,
tickQueue执行, 打印 11
微任务执行,打印12
check队列执行,
打印 13
TickQueue中加入
执行promise, 打印 14
加入微任务
当前阶段执行完毕,
tickQueue执行, 打印 15
微任务执行,打印16
完事,上面这个例子一定可以帮助你理解Node事件队列,相信我,自己启动node执行环境试下,详细分析一下。
Node与浏览器的 Event Loop 差异:
浏览器环境下,microtask的任务队列是每个macrotask都执行完之后执行。 而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是macro中 一个阶段执行完毕,就会去执行microtask队列的任务。
浏览器和Node 环境下,microtask 任务队列的执行时机不同
Node端,microtask 在事件循环的各个阶段之间执行 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行 由于node版本更新到11,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这点就跟浏览器端一致。
知识拓展
异步执行的运行机制
- (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- (2)主线程之外,还存在一个"任务队列"(task queue & macrotask queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- (4)主线程不断重复上面的第三步
"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数
除了放置异步任务的事件,"任务队列"还可以放置定时事件,需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行.