相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)javascript
javascript是一门单线程语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变,无论谁写的代码,都得一句一句的来执行。html
当咱们打开网站时,网页的渲染过程包括了一大堆任务,好比页面元素的渲染。script脚本的执行,经过网络请求加载图片音乐之类。若是一个一个的顺序执行,赶上任务耗时过长,就会发生卡顿现象。因而,事件循环(Event Loop)应运而生。前端
事件循环,能够理解为实现异步的一种方式。event loop在HTML Standard中的定义:java
为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的
event loop
。node
JavaScript
有一个主线程 main thread
,和调用栈 call-stack
也称之为执行栈。全部的任务都会放到调用栈中等待主线程来执行。待执行的任务就是流水线上的原料,只有前一个加工完,后一个才能进行。event loops就是把原料放上流水线的工人,协调用户交互,脚本,渲染,网络这些不一样的任务。git
将待执行任务分为两类:github
主线程自上而下执行全部代码web
Event Table
并注册相对应的回调函数Event Table
会将这个函数移入 Event Queue
Event Queue
中读取任务,进入到主线程去执行。一个event loop有一个或者多个task队列。当用户代理安排一个任务,必须将该任务增长到相应的event loop的一个tsak队列中。面试
task也被称为macrotask(宏任务),是一个先进先出的队列,由指定的任务源去提供任务。编程
task任务源很是宽泛,总结来讲task任务源包括:
因此 Task Queue
就是承载任务的队列。而 JavaScript
的 Event Loop
就是会不断地过来找这个 queue
,问有没有 task
能够运行运行。
每个event loop都有一个microtask队列,一个microtask会被排进microtask队列而不是task队列。
microtask 队列和task 队列有些类似,都是先进先出的队列,由指定的任务源去提供任务,不一样的是一个 event loop里只有一个microtask 队列。
一般认为是microtask任务源有:
在Promises/A+规范的Notes 3.1中说起了promise的then方法能够采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。因此不一样浏览器对promise的实现可能存在差别。
事件循环的顺序,决定js代码的执行顺序。进入总体代码(宏任务)后,开始第一次循环。接着执行全部的微任务。而后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行全部的微任务。
执行完
microtask
队列里的任务,有可能会渲染更新。(浏览器很聪明,在一帧之内的屡次dom变更浏览器不会当即响应,而是会积攒变更以最高60HZ的频率更新视图)
以下面代码,setTimeout
就是一个异步任务,
console.log('start')
setTimeout(()=>{
console.log('setTimeout')
});
console.log('end');
复制代码
console.log('start');
setTimeout
发现是一个异步任务,就先注册了一个异步的回调console.log('end')
Event Queue
是否有待执行的 task,只要主线程的task queue
没有任务执行了,主线程就一直在这等着console.log('setTimeout')
。js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
注意,只有等主线程执行完毕,才会检查Event Queue
是否有待执行的 task,所以可能会出现另外一种状况。
console.log('start')
setTimeout(()=>{
console.log('setTimeout')
}, 3000);
todo(); // 假定这里是一个耗时10秒的操做
复制代码
正常状况下,控制台输出应该是这样的
start
// 等待3秒
setTimeout
复制代码
而实际上,输出大概是这样的:
start
// 等待10秒
setTimeout
复制代码
从新分析一下执行流程:
console.log('start');
setTimeout
发现是一个异步任务,就先注册了一个异步的回调todo()
timeout
完成,打印任务进入Event QueueEvent Queue
是否有待执行的 task,console.log('setTimeout')
。setTimeout
这个函数,是通过指定时间后,把要执行的任务加入到Event Queue中,与上一个栗子不一样,当计时事件完成后,主线程任务并无执行完毕。只有等主线程执行完本轮代码后,才会查询Event Queue。
因此,等待大约10秒后控制台才有第二次输出。
setTimeout(fn,0)
setTimeout(fn,0)
的含义是,指定某个任务在主线程最先可得的空闲时间执行,意思就是只要主线程执行栈内的同步任务所有执行完成,栈为空就立刻执行。
即使主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。
setInterval
会每隔指定的时间将注册的函数置入Event Queue,若是前面的任务耗时过久,那么一样须要等待。
与setTimeout
类似,对于setInterval(fn,ms)
来讲,不是每过ms
秒会执行一次fn
,而是每过ms
秒,会有fn
进入Event Queue。一旦**setInterval
的回调函数fn
执行时间因为主线程繁忙超过了延迟时间ms
,那么就彻底看不出来有时间间隔,而是会连续执行。**
process.nextTick(callback)
相似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。
以一段代码为例:
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
复制代码
主线程自上而下执行全部代码
setTimeout
,那么将其回调函数注册后分发到宏任务Event Queue。Promise
,new Promise
当即执行,then
函数分发到微任务Event Queue。console.log()
,当即执行。then
在微任务Event Queue里面,执行。setTimeout
对应的回调函数,当即执行。执行完一个宏任务后,会执行全部的微任务,而后再执行一个宏任务
console.log('start');
Promise.resolve()
.then(function promise1() { // then1
console.log('promise1');
})
.then(function () { // then2
console.log('promise2')
})
setTimeout(function setTimeout1() { // setTimeout1
console.log('setTimeout1')
Promise.resolve().then(function promise2() { // then3
console.log('promise3');
})
}, 0)
setTimeout(function setTimeout2() { // setTimeout1
console.log('setTimeout2')
}, 0)
console.log('end')
复制代码
分析下执行流程:
console.log()
,当即执行, 输出 start。Promise
,then
被分发到微任务Event Queue中。咱们记为then1
。setTimeout
,其回调函数被分发到宏任务Event Queue中。咱们暂且记为setTimeout1
。setTimeout
,其回调函数被分发到宏任务Event Queue中,咱们记为setTimeout2
。console.log()
,当即执行, 输出 end。第一轮执行结束,控制台输出 stert,end,此时,任务队列以下:
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | then1 |
setTimeout2 |
执行微任务:
微任务执行完毕,第二轮循环开始,转入宏任务 setTimeout1:
console.log()
,当即执行, 输出 setTimeout1。Promise
,then
被分发到微任务Event Queue中。咱们记为then3
。执行微任务 then3:
console.log()
,当即执行, 输出 promise3。第三轮循环开始,执行宏任务setTimeout2
console.log()
,当即执行, 输出 setTimeout2最后,控制台输出结果为
stert
end
promise1
promise2
setTimeout1
promise3
setTimeout2
复制代码
Node中的Event Loop
是基于libuv
实现的,而libuv
是 Node 的新跨平台抽象层,libuv
使用异步,事件驱动的编程方式,核心是提供i/o
的事件循环和异步回调。libuv
的API
包含有时间,非阻塞的网络,异步文件操做,子进程等等。
Node 的 Event Loop 分为 6 个阶段:
setTimeout()
和 setInterval()
中到期的callback。I/O
callback会被延迟到这一轮的这一阶段执行I/O
callback,在适当的条件下会阻塞在这个阶段setImmediate
的callbackclose
事件的callback,例如socket.on('close'[,fn])
、http.server.on('close, fn)
上面六个阶段都不包括 process.nextTick()
timers 阶段会执行 setTimeout
和 setInterval
回调,而且是由 poll 阶段控制的。
在 timers 阶段其实使用一个最小堆而不是队列来保存全部的元素,由于timeout的callback是按照超时时间的顺序来调用的,并非先进先出的队列逻辑)。而为何 timer 阶段在第一个执行阶梯上其实也不难理解。在 Node 中定时器指定的时间也是不许确的,而这样,就能尽量的准确了,让其回调函数尽快执行。
pending callbacks 阶段实际上是 I/O
的 callbacks 阶段。好比一些 TCP 的 error 回调等。
poll 阶段主要有两个功能:
I/O
回调当时Event Loop 进入到 poll 阶段而且 timers 阶段没有任何可执行的 task 的时候(也就是没有定时器回调),将会有如下两种状况
check 阶段在 poll 阶段以后,setImmediate()
的回调会被加入check队列中,他是一个使用libuv API
的特殊的计数器。
一般在代码执行的时候,Event Loop 最终会到达 poll 阶段,而后等待传入的连接或者请求等,可是若是已经指定了setImmediate()而且这时候 poll 阶段已经空闲的时候,则 poll 阶段将会被停止而后开始 check 阶段的执行。
若是一个 socket 或者事件处理函数忽然关闭/中断(好比:socket.destroy()
),则这个阶段就会发生 close
的回调执行。
setImmediate
在 poll 阶段后执行,即check 阶段setTimeout
在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段计时器的执行顺序将根据调用它们的上下文而有所不一样。 若是二者都是从主模块中调用的,则时序将受到进程性能的限制。
若是不在I / O
周期(即主模块)内,则两个计时器的执行顺序是不肯定的,由于它受进程性能的约束:
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
复制代码
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
若是在一个I/O
周期内移动这两个调用,则始终首先执行当即回调:
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
与setTimeout()
相比,使用setImmediate()
的主要优势是,若是在I / O
周期内安排了任何计时器,则setImmediate()
将始终在任何计时器以前执行,而与存在多少计时器无关。
process.nextTick()
从技术上讲不是Event Loop的一部分。 相反,不管当前事件循环的当前阶段如何,若是存在 nextTickQueue
,都将在当前操做完成以后处理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
复制代码
浏览器环境下,microtask的任务队列是每一个macrotask执行完以后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
若是你收获了新知识,请给做者点个赞吧,左侧边栏第一个按钮,用力的点一下~
参考文章: