"Event Loop是一个程序结构,用于等待和发送消息和事件。 (a programming construct that waits for and dispatches events or messages in a program.)"
复制代码
举一个你们都熟知的栗子, 这样更能客观的理解。node
你们都知道深夜食堂吧。厨师就一我的(服务端 Server)。最多再来一个服务生( 调度员 Event Loop )。晚上吃饭的客人(客户端 Client)不少。数据库
1. 客人向服务生点完菜,就干本身事情,不用一直等着服务生, 服务生把一我的点的菜单送到厨师,
又去服务新的客人...
2. 厨师(服务端)只负责作客人们点的菜。
3. 服务生(调度员)不停的看厨师,一旦厨师作好菜了,按照标号送到相应的 客人(客户端)座位上
复制代码
主线程
, 厨师线程 为
消息线程
。 客人每点一个菜。服务生就向厨师发出一个消息。并保留该消息的“标识”(
回调函数
)用来接收厨师炒好的菜,并把菜送到相应的客人手中。
无论店里的客人多少,也无论每一份菜须要多久的时间作好。就只有厨师这一我的忙活。厨师一次只能服务一个客人。那这样的服务模式效率就比较低了。中途等待的时间比较长。 笔者认为 同步模式 就是没有 “服务生线程”, 厨师线程升级为 主线程
。api
1. 第一个客人点了一份 "读取文件" , 炒好一份 "读取文件" 须要花费 1 分钟
2. 必须等第一个客人的菜炒好后,第二个客人才能点,而且点了一份 "读取数据库",
炒好一份 "读取数据库" 须要花费 2 分钟
3. 第三个客人点了一份 ...
复制代码
从图中能够看出红色部份都是等待时间(或者是阻塞时间), 至关浪费资源。promise
假设咱们如今只知道一种代码的执行方式 "同步执行", 也就是代码从上到下 按顺序执行。若是遇到 setTimeout , 也先这样理解。(实际上setTimeout 自己是当即执行的,只是回调函数异步执行)浏览器
console.log(1); //执行顺序1
setTimeout(function(){}, 1000); //执行顺序2
console.log(2); //执行顺序3
复制代码
图表更能直观的反应这个概念: bash
主线程
不停的接收请求 request 和 响应请求 response, 真正处理任务的被 消息线程 event loop
安排其余相应的程序去执行,并接收相应的相应程序返回的消息。而后 reponse 给客户端。多线程
1. 主线程干的事情很是简单,即 接收请求,响应请求, 所以能够可以处理更多的请求。而不用等待。
2. 消息线程维护请求,并把真正要作的事情交给对应的程序,并接收对应程序的回调消息,返回给 主线程
复制代码
你跟你的女神表白,你女神当即回复你,而你也一直再等女神的回复异步
你跟你的女神表白, 你表白后,没有等女神来得及回复,你去忙你本身的事情了。你的女神当即回复了你socket
你跟你的女神表白, 你女神没有当即回复你,说要考虑考虑,过几天答复你,而你也一直再等女神的回复函数
你跟你的女神表白,你表白后, 没有等女神的回复。你去忙你本身的事情了,女神也说她要考虑考虑,过几天再回复你
阻塞非阻塞 是指调用者
(表白的那我的) 同步异步 是指被调用者
(被表白的那我的)
宏任务 setTimeout , setInterval, setImmediate, I / O 操做
微任务 process.nextTick , 原生Promise (有些实现的Promise将then方法放到了宏任务中), Mutation Observer
console.log(1);
Promise.resolve('123').then(()=>{console.log('then')})
process.nextTick(function () {
console.log('nextTick')
})
console.log(2);
复制代码
process.nextTick 优先于 promise.then 方法执行
function one() {
let a = 1;
two();
function two() {
console.log(a);
let b = 2;
function three() {
//debugger;
console.log(b);
}
three();
}
}
one();
复制代码
毫无疑问的是,上面这段代码执行的结果为:
1
2
复制代码
在栈中都是以同步任务的方式存在:
再来看下面这段代码:
console.log(1);
setTimeout(function(){
console.log(2);
})
console.log(3);
复制代码
执行结果为:
1
3
2
复制代码
那究竟是怎样执行的呢?
//宏任务
setTimeout(function(){
console.log(2);
})
//微任务
let p = new Promise((resolve, reject) => {
resolve(3);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
console.log(4);
复制代码
执行结果为:
1
4
3
2
复制代码
从这个能够看到。微任务消息队列的执行的
优先
于宏任务的消息队列.
console.log(1);
//宏任务
setTimeout(function(){
console.log(2);
})
//微任务
let p = new Promise((resolve, reject) => {
resolve(4);
});
p.then((data) => {
console.log(data);
}, (err)=>{
})
setTimeout(function(){
console.log(3);
})
console.log(5);
复制代码
执行结果为:
1
5
4
2
3
复制代码
每一次事件循环机制过程当中,会将当前宏任务 或者 微任务消息队列中的任务都执行完成。而后再以前其余队列。
当前
的全部任务(同步任务)都已经执行完成。咱们先来看看node是怎样运行的:
Event Loop 在整个Node 运行机制中占据着举足轻重的地位。是其核心。
每一个阶段都有一个执行回调的FIFO队列。 虽然每一个阶段都有其特定的方式,但一般状况下,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操做, 而后在该阶段的队列中执行回调,直到队列耗尽或回调的最大数量 已执行。 当队列耗尽或达到回调限制时,事件循环将移至下一个阶段,依此类推。
timers:此阶段执行由setTimeout()和setInterval()调度的回调。
pending callbacks:执行I / O回调,推迟到下一个循环迭代。
idle,prepare:只在内部使用。
poll:检索新的I / O事件; 执行I / O相关的回调函数; 适当时节点将在此处阻塞。
check:setImmediate()回调在这里被调用。
close backbacks:一些关闭回调,例如 socket.on('close',...)。
复制代码
timers阶段
须要注意的是:
const fs = require('fs');
function someAsyncOperation(callback) {
//假设须要95ms须要执行完成
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
//定义100ms后执行
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// 执行someAsyncOperation须要消耗95ms执行
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
复制代码
分析上述代码:
pending callbacks阶段
此阶段为某些系统操做(如TCP错误类型)执行回调。例如,
若是尝试链接时TCP套接字收到ECONNREFUSED,则某些* nix系统要等待报告错误。这将排队等候在待处理的回调阶段执行。
复制代码
poll阶段
1.计算应该阻塞和轮询I / O的时间
2.处理轮询队列中的事件。
复制代码
当事件循环进入poll阶段而且没有计时器时,会发生如下两件事之一:
1. 若是轮询队列不为空,则事件循环将遍历其回调队列,同步执行它们,直到队列耗尽或达到系统相关硬限制。
2. 若是轮询队列为空,则会发生如下两件事之一:
2.1 若是脚本已经过setImmediate()进行调度,则事件循环将结束轮询阶段并继续执行(check阶段)检查阶段以执行这些预约脚本。
2.2 若是脚本没有经过setImmediate()进行调度,则事件循环将等待将回调添加到队列中,而后当即执行它们。
复制代码
一旦轮询队列为空,事件循环将检查已达到时间阈值的定时器。若是一个或多个定时器准备就绪,则事件循环将回退到定时器阶段以执行这些定时器的回调。
// poll的下一个阶段时check
// 有check阶段就会走到check中
let fs = require('fs');
fs.readFile('./1.txt',function () { //轮询队列已经执行完成,为空,即2.1中描述的
setTimeout(() => {
console.log('setTimeout')
}, 0);
setImmediate(() => {
console.log('setImmediate')
});
});
复制代码
上面这段代码执行的过程阶段为:
check阶段
setImmediate()其实是一个特殊的定时器,它在事件循环的一个单独的阶段中运行。它使用libuv API来调度回调,以在轮询(poll)阶段完成后执行。
close callback阶段
若是套接字socks或句柄忽然关闭(例如socket.destroy()),则在此阶段将发出'close'事件。 不然它将经过process.nextTick()触发事件。
复制代码
setImmediate()用于在当前轮询阶段完成后执行脚本。
setTimeout()计划脚本在通过最小阈值(以毫秒为单位)后运行。
复制代码
定时器执行的顺序取决于它们被调用的上下文。 若是二者都是在主模块内调用的,那么时序将受到进程性能的限制(可能会受到计算机上运行的其余应用程序的影响)。
简言之: setTimediate 和 setTimeout 的执行顺序不肯定。
// setTimeout和setImmediate顺序是不固定,看node准备时间
setTimeout(function () {
console.log('setTimeout')
},0);
setImmediate(function () {
console.log('setImmediate')
});
复制代码
输出的结果多是这样
setTimeout
setImmediate
复制代码
也有多是这样
setImmediate
setTimeout
复制代码
But, 若是在I / O周期内移动这两个调用,则当即回调老是首先执行, 能够爬楼参考 poll阶段的介绍。
使用setImmediate()的主要优势是,若是在I / O周期内进行调度,将始终在任何计时器以前执行setImmediate(),而无论有多少个计时器。
为何要用process.nextTick
容许用户处理错误,清理任何不须要的资源,或者可能在事件循环继续以前再次尝试请求。 有时须要在调用堆栈解除以后但事件循环继续以前容许回调运行。
process.nextTick()没有显示在图中,即便它是异步API的一部分。 这是由于process.nextTick()在技术上并非事件循环的一部分。 相反,nextTickQueue将在当前操做完成后处理,而无论事件循环的当前阶段如何。
回顾一下事件循环机制,只要你在给定的阶段调用process.nextTick(),全部传递给process.nextTick()的回调都将在事件循环继续以前被解析。
// nextTick是队列切换时执行的,timer->check队列 timer1->timer2不叫且
setImmediate(() => {
console.log('setImmediate1')
setTimeout(() => {
console.log('setTimeout1')
}, 0);
})
setTimeout(()=>{
process.nextTick(()=>console.log('nextTick'))
console.log('setTimeout2')
setImmediate(()=>{
console.log('setImmediate2')
})
},0);
复制代码
在讨论事件循环(Event Loop)的时候,要时刻知道 宏任务,微任务,process.nextTick等概念。 上面代码执行的结果可能为:
setTimeout2
nextTick
setImmediate1
setImmediate2
setTimeout1
复制代码
或者
setImmediate1
setTimeout2
setTimeout1
nextTick
setImmediate2
复制代码
为何呢? 这个就留给各位看官的一个思考题吧。欢迎留言讨论~