在说EventLoop以前咱们先看一道题html
setTimeout(() => {
console.log(111);
}, 1000);
while (true) {
console.log(22);
}
复制代码
console.log(111); 永远都不会输出,由于javaScript 是单线程html5
咱们常常说JS是单线程执行的,指的是一个进程里只有一个主线程,那到底什么是线程?什么是进程?java
官方的说法是: 进程是CPU资源分配的最小单位;线程是CPU调度的最小单位。 这两句话并很差理解,咱们先来看张图:node
以 Chrome 浏览器中为例,当你打开一个 Tab 页时,其实就是建立了一个进程,一个进程中能够有多个线程(下文会详细介绍),好比渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是建立了一个线程,当请求结束后,该线程可能就会被销毁ios
主程序只有一个线程,即同一时间片断内其只能执行单个任务。ajax
JavaScript的主要用途是与用户交互,以及操做DOM,若是一个线程是执行删除操做,一个是修改操做,那么就会出现问题。所以决定了它只能是单线程,不然会带来不少复杂的同步问题。axios
单线程就意味着,同一时间只能执行一个任务,全部任务都须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就须要一直等着。这就会致使IO操做(耗时但CPU闲置)时形成性能浪费的问题。promise
答案是异步 ,主线程彻底能够无论IO操做,暂时挂起处于等待中的任务,先运行排在后面的任务。等到IO操做返回告终果,在回过头,把挂起的任务继续执行下去。因而,因此任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。浏览器
简单来讲浏览器内核是经过取得页面内容,整理信息(应用CSS),计算和组合最终输出可视化的图像结果,一般也被成为渲染引擎。markdown
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器一般由一下常驻线程组成:
事件循环中的异步队列有两种:macro(宏任务)队列和micro(微任务)队列。宏任务队列能够有多个,微任务队列只有一个
一个完整的Event Loop过程,能够归纳为如下阶段:
一开始执行栈空,咱们能够把 执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro队列空,macro队列里有且只有script脚本(总体代码)。
全局上下文(script标签)被推入执行栈,同步代码执行。在执行的过程当中,会判断是同步任务仍是异步任务,经过对一些接口的调用,能够产生新的macro-task与micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script脚本会被移出macro队列,这个过程本质上是队列的macro-task的执行和出队的过程。
上一步咱们出队的是一个macro-task,这一步咱们处理的是micro-task。但须要注意的是:当macro-task出队时,任务是一个一个执行的;而micro-task出队时,任务时一队一队执行的。所以,咱们处理micro队列这一步,会逐个执行队列中的任务并把它出队,知道队列被清空。
执行渲染操做,更新界面
检查是否存在Web worker任务,若是有,则对其进行处理
上述过程循环往复,知道两个队列都清空
咱们总结一下,每次循环都是一个这样的过程:
当某个宏任务执行完后,会查看是否有微任务队列。若是有,先执行微任务队列中的全部任务,若是没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程当中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。
接下来咱们看道例子来介绍上面流程:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
复制代码
最后输出结果是 Promise1,setTimeout1,Promise2,setTimeout2
一开始执行栈的同步任务(这属于宏任务)执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),而后执行微任务队列中的全部任务输出 Promise1,同时会生成一个宏任务 setTimeout2
而后去查看宏任务队列,宏任务 setTimeout1 在 setTimeout2 以前,先执行宏任务 setTimeout1,输出 setTimeout1
在执行宏任务 setTimeout1 时会生成微任务 Promise2 ,放入微任务队列中,接着先去清空微任务队列中的全部任务,输出 Promise2
清空完微任务队列中的全部任务后,就又会去宏任务队列取一个,这回执行的是 setTimeout2
Node中的Event Loop和浏览器中的是彻底不相同的东西。Node.js采用V8做为js的解析引擎,而I/O处理方面使用了本身设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不一样操做系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现:
Node.js的运行机制以下:
其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
从上图中,大体看出 node 中的事件循环的顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O 事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段(按照该顺序反复运行)...
注意:上面六个阶段都不包括 process.nextTick()(下文会介绍)
接下去咱们详细介绍timers、poll、check这 3 个阶段,由于平常开发中的绝大部分异步任务都是在这 3 个阶段处理的。
timers 阶段会执行 setTimeout 和 setInterval 回调,而且是由 poll 阶段控制的。
一样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。
poll 是一个相当重要的阶段,这一阶段中,系统会作两件事情
而且在进入该阶段时若是没有设定了 timer 的话,会发生如下两件事情
固然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,若是有的话会回到 timer 阶段执行回调。
setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图能够知道,check 阶段的执行顺序在 poll 阶段以后。
咱们先来看个例子:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
复制代码
一开始执行栈的同步任务(这属于宏任务)执行完毕后(依次打印出 start end,并将 2 个 timer 依次放入 timer 队列),会先去执行微任务(这点跟浏览器端的同样),因此打印出 promise3
而后进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise.then 回调放入 microtask 队列,一样的步骤执行 timer2,打印 timer2;这点跟浏览器端相差比较大,timers 阶段有几个 setTimeout/setInterval 都会依次执行,并不像浏览器端,每执行一个宏任务后就去执行一个微任务(关于 Node 与浏览器的 Event Loop 差别,下文还会详细介绍)。
两者很是类似,区别主要在于调用时机不一样。
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
复制代码
对于以上代码来讲,setTimeout 可能执行在前,也可能执行在后。
首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的
进入事件循环也是须要成本的,若是在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调
若是准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了
但当两者在异步 i/o callback 内部调用时,老是先执行 setImmediate,再执行 setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
复制代码
在上述代码中,setImmediate 永远先执行。由于两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,因此就直接跳转到 check 阶段去执行回调了。
这个函数实际上是独立于 Event Loop 以外的,它有一个本身的队列,当每一个阶段完成后,若是存在 nextTick 队列,就会清空队列中的全部回调函数,而且优先于其余 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 队列的任务。
接下咱们经过一个例子来讲明二者区别:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
复制代码
浏览器端运行结果:timer1=>promise1=>timer2=>promise2
浏览器端的处理过程以下:
Node 端运行结果:timer1=>timer2=>promise1=>promise2
全局脚本(main())执行,将 2 个 timer 依次放入 timer 队列,main()执行完毕,调用栈空闲,任务队列开始执行;
首先进入 timers 阶段,执行 timer1 的回调函数,打印 timer1,并将 promise1.then 回调放入 microtask 队列,一样的步骤执行 timer2,打印 timer2;
至此,timer 阶段执行结束,event loop 进入下一个阶段以前,执行 microtask 队列的全部任务,依次打印 promise一、promise2
Node 端的处理过程以下:
浏览器和 Node 环境下,microtask 任务队列的执行时机不一样
setTimeout(()=>{
console.log(1)
},0)
let a=new Promise((resolve)=>{
console.log(2)
resolve()
}).then(()=>{
console.log(3)
}).then(()=>{
console.log(4)
})
console.log(5)
复制代码
以此输出 2,5,3,4,1
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
复制代码
promise1,then11,promise2,then21,then12,then23
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
return new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
复制代码
Promise的第二个then至关因而挂在新Promise的最后一个then的返回值上。
promise1,then11,promise2,then21,then23,then12
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then11")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then21")
}).then(()=>{
console.log("then23")
})
}).then(()=>{
console.log("then12")
})
new Promise((resolve,reject)=>{
console.log("promise3")
resolve()
}).then(()=>{
console.log("then31")
})
复制代码
promise1,promise3,then11,promise2,then31,then21,then12,then23
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
复制代码
script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout
async1 能够当作以下
funcation async1(){
console.log("async1 start");
new Promise((resolve)=>{
console.log( 'async2');
}).then(()=>{
console.log("async1 end");
})
}
复制代码
async function async1() {
console.log(1)
await async2()
console.log(2)
return await 3
}
async function async2() {
console.log(4)
}
setTimeout(function() {
console.log(5)
}, 0)
async1().then(v => console.log(v))
new Promise(function(resolve) {
console.log(6)
resolve();
console.log(7)
}).then(function() {
console.log(8)
})
console.log(9)
复制代码
1,4,6,7,9,2,8,3,5
咱们知道Promise自己是一个异步方法,必须得在执行栈执行完了再去取它的值,所以,全部的返回值都得包一层异步setTimeout。那么问题来了,为何Promise的resolve被setTimeout包裹后就成了微任务,要知道setTimeout但是宏任务。
在现代浏览器里面,产生微任务有两种方式。
第一种是使用MutationObserver监控某个DOM节点,而后在经过JavaScript来修改这个节点,或者为这个节点添加,删除部分子节点,当DOM节点发生变化时,就会产生DOM变化记录的微任务。
第二种方式是使用Promise,当调用Promise.resolve()或者Promise.reject()的时候,也会产生微任务。
ECMAScript规范明确指出Promise必须以Promise Job形式加入job queues(也就是microtask)。Job Queue是ES6中新剔除的概念,创建在事件循环队列之上。