讨论event loop要做到以下两点
- 首先要确定好上下文,nodejs和浏览器的event loop是两个有明确区分的事物,不能混为一谈。
- 其次,讨论一些js异步代码的执行顺序时候,要基于node的源码而不是自己的臆想。
简单来讲:
- nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
- libuv已经对event loop作出了实现,而html5规范中只是定义了浏览器中event loop的模型,具体实现留给了浏览器厂商。
浏览器中的event loop
对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走(注意:栈里的代码是先进后出),走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。
简单来讲:
- 1.所有同步任务都在主线程上执行,形成一个执行栈
- 2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
- 4.主线程从任务队列中读取事件,这个过程是循环不断的
整个的这种运行机制又称为Event Loop(事件循环)
概念中首先要明白是:stack(栈)和queue(队列)的区别,它们是怎么去执行的?
栈方法LIFO(Last In First Out):先进后出(先进的后出),典型的就是函数调用。
//执行上下文栈 作用域
var a = "aa";
function one(){
let a = 1;
two();
function two(){
let b = 2;
three();
function three(){
console.log(b)
}
}
}
console.log(a);
one();
aa
2
图解执行原理:
那么怎么出呢,怎么销毁的呢?
最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。
队列方法FIFO(First In First Out)
(队头)[1,2,3,4](队尾) 进的时候从队尾依次进1,2,3,4 出的时候从对头依次出1,2,3,4

浏览器事件环中代码执行都是按栈的结果去执行的,但是我们调用完多线程的方法(WebAPIs),这些多线程的方法是放在队列里的,也就是先放到队列里的方法先执行。
那什么时候WebAPIs里的方法会再执行呢?
比如:stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调用了WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。
下面通过列子来说明:
例子1
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
})
setTimeout(function(){
console.log(4);
})
console.log(5);
// 结果
1
2
5
3
4
1、首先执行栈里面的同步代码
2
5
2、栈里面的setTimeout事件会依次放到任务队列中,当栈里面都执行完之后,再依次从从任务队列中读取事件往栈里面去执行。
3
4
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
setTimeout(function(){
console.log(6);
})
})
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
})
console.log(5)
// 结果
1
2
5
3
4
6
7
1、首先执行栈里面的同步代码
1
2
5
2、栈里面的setTimeout事件会依次放到任务队列中,当栈里面都执行完之后,再依次从从任务队列中读取事件往栈里面去执行。
3
4
3、当执行栈开始依次执行setTimeout时,会将setTimeout里面的嵌套setTimeout依次放入队列中,然后当执行栈中的setTimeout执行完毕后,再依次从从任务队列中读取事件往栈里面去执行。
6
7
例子3
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3);
setTimeout(function(){
console.log(6);
})
},400)
setTimeout(function(){
console.log(4);
setTimeout(function(){
console.log(7);
})
},100)
console.log(5)
// 结果
1
2
5
4
7
3
6
在例子2的基础上,如果设置了setTimeout的时间,那就是按setTimeout的成功时间依次执行。
如上:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(因为只有当时间到了的时候,才会把set放到队列里面去,这一点跟nodejs中的set设置了时间的机制差不多,可以看nodejs中的例子6,也是会先走完时间短,再走时间慢的。)
例子4
当触发回调函数时,会将回调函数放到队列中。永远都是栈里面执行完后再从任务队列中读取事件往栈里面去执行。
setTimeout(function(){
console.log('setTimeout')
},4)
for(var i = 0;i<10;i++){
console.log(i)
}
// 结果
0
1
2
3
4
5
6
7
8
9
setTimeout
在学习nodejs事件环之前,我们先了解一下宏任务和微任务在浏览器中的执行机制。也是面试中经常会被问到的。
宏任务和微任务

任务可分为宏任务和微任务,宏任务和微任务都是队列
- macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O
- micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver不兼容的,MessageChannel(消息通道,类似worker)
Promise.then(源码见到Promise就用setTimeout),then方法不应该放到宏任务中(源码中写setTimeout是迫不得已的),默认浏览器的实现这个then放到了微任务中。例如:
console.log(1)
let promise = new Promise(function(resolve,reject){
console.log(3)
resolve(100)
}).then(function(data){
console.log(100)
})
console.log(2)
1
3
2
100
先走console.log(1),这里的new Promise()是立即执行的,所以是同步的,由于这个then在console.log(2)后面执行的,所以不是同步,是异步的。
那这跟宏任务和微任务有什么关系?
我们可以加一个setTimeout(宏任务)对比一下:
console.log(1)
setTimeout(function(){
console.log('setTimeout')
},0)
let promise = new Promise(function(resolve,reject){
console.log(3)
resolve(100)
}).then(function(data){
console.log(100)
})
console.log(2)
1
3
2
100
setTimeout
结论:在浏览器事件环机制中,同步代码先执行 执行是在栈中执行的,然后微任务会先执行,再执行宏任务
MutationObserver例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<!-- 当dom加载完毕后,来一句渲染完成 -->
<script>
console.log(1)
let observe = new MutationObserver(function(){
console.log('渲染完成')
});
<!--监控app的节点列表是否渲染完成-->
observe.observe(app,{
childList:true
})
for(var i = 0;i<100;i++){
let p = document.createElement('p');
document.getElementById('app').appendChild(p);
}
for(var i = 0;i<100;i++){
let p = document.createElement('p');
document.getElementById('app').appendChild(p);
}
console.log(2)
</script>
</body>
</html>
// 结果
1
2
渲染完成
MessageChannel例子
vue中nextTick的实现原理就是通过这个方法实现的
console.log(1);
let channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.onmessage = function(e){
console.log(e.data);
}
console.log(2);
port2.postMessage(100);
console.log(3)
// 浏览器中console结果 会等所有同步代码执行完再执行,所以是微任务晚于同步的
1
2
3
100
nodejs中的event loop
node的特点:异步 非阻塞i/o node通过LIBUV这个库自己实现的异步,默认的情况下是没有异步的方法的。
nodejs中的event loop有6个阶段,这里我们重点关注poll阶段(fs的i/o操作,对文件的操作,i/o里面的回调函数都放在这个阶段)

下面通过列子来说明:
例子1
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise')
})
})
setTimeout(function(){
console.log('setTimeout2')
})
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise
图解执行原理:

1、首先执行完栈里面的代码
console.log(1);
console.log(2);
2、从栈进入到event loop的timers阶段,由于nodejs的event loop是每个阶段的callback执行完毕后才会进入下一个阶段,所以会打印出timers阶段的两个setTimeout的回调
setTimeout1
setTimeout2
3、由于node event中微任务不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。所以当times阶段的callback执行完毕,准备切换到下一个阶段时,执行微任务(打印出Piromise),
Promise
如果例子1看懂了,以下例子2-例子6自己走一遍。需要注意的是例子6,当setTimeout设置了时间,优先按时间顺序执行(浏览器事件环中例子3差不多)。例子7,例子8是重点。
例子2
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise')
})
},1000)
setTimeout(function(){
console.log('setTimeout2')
},1000)
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise
例子3
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise')
})
},2000)
setTimeout(function(){
console.log('setTimeout2')
},1000)
-> node eventloop.js
1
2
setTimeout2
setTimeout1
Promise
例子4
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise1')
})
})
setTimeout(function(){
console.log('setTimeout2')
Promise.resolve().then(function(){
console.log('Promise2')
})
})
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
例子5
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise1')
})
},1000)
setTimeout(function(){
console.log('setTimeout2')
Promise.resolve().then(function(){
console.log('Promise2')
})
},1000)
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
例子6
console.log(1);
console.log(2);
setTimeout(function(){
console.log('setTimeout1')
Promise.resolve().then(function(){
console.log('Promise1')
})
},2000)
setTimeout(function(){
console.log('setTimeout2')
Promise.resolve().then(function(){
console.log('Promise2')
})
},1000)
-> node eventloop.js
1
2
setTimeout2
Promise2
setTimeout1
Promise1
例子7:setImmediate() vs setTimeout()
- setImmediate 设计在poll阶段完成时执行,即check阶段;
- setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
-> node eventloop.js
timeout
immediate
-> node eventloop.js
immediate
timeout
但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout
这是因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
$ node eventloop.js
immediate
timeout
例子8:process.nextTick()
process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
function Fn(){
this.arrs;
process.nextTick(()=>{
this.arrs();
})
}
Fn.prototype.then = function(){
this.arrs = function(){console.log(1)}
}
let fn = new Fn();
fn.then();
-> node eventloop.js
1
不加process.nextTick,new Fn()的时候,this.arrs是undefind,this.arrs()执行会报错;
加了process.nextTick,new Fn()的时候,this.arrs()不会执行(因为process.nextTick是微任务,只有在各个阶段切换的中间执行,所以它会等到同步代码执行完之后才会执行)这个时候同步代码fn.then()执行=>this.arrs = function(){console.log(1)},this.arrs变成了一个函数,同步执行完后再去执行process.nextTick(()=>{this.arrs();})就不会报错。
需要注意的是:nextTick千万不要写递归,可以放一些比setTimeout优先执行的任务
// 死循环,会一直执行微任务,卡机
function nextTick(){
process.nextTick(function(){
nextTick();
})
}
nextTick()
setTimeout(function(){
},499)
最后再来段代码加深理解
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{
console.log('nextTick1');
})
process.nextTick(()=>{
console.log('nextTick2');
})
});
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
1、从poll —> check阶段,先执行process.nextTick,
nextTick1
nextTick2
2、然后进入check,setImmediate,
setImmediate
3、执行完setImmediate后,出check,进入close callback前,执行process.nextTick
nextTick3
4、最后进入timer执行setTimeout
setTimeout
结论:在nodejs事件环机制中,微任务是在各个阶段切换的中间去执行的。
最后
-
在浏览器的事件环机制中,我们需要了解的是栈和队列是怎么去执行的。
栈:先进后出;队列:先进先出。
所有代码在栈中执行,栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,有微任务先会将微任务依次从队列中取出放到执行栈中执行,最后再依次将队列中的事件放到执行栈中依次执行。
-
在nodejs的事件环机制中,我们需要了解的是node的执行机制是阶段型的,微任务不属于任何阶段,而是在各个阶段切换的中间执行。nodejs把事件环分成了6阶段,这里需要注意的是,当执行栈里的同步代码执行完毕切换到node的event loop时也属于阶段切换,这时候也会先去清空微任务。
-
微任务和宏任务
macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O
micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver不兼容的
问题
如果在执行宏任务的过程中又发现了回调中有微任务,会把这个微任务提前到所有宏任务之前,等到这个微任务完成后再继续执行宏任务吗?
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise1')
})
})
setTimeout(function(){
console.log(3)
Promise.resolve(1).then(function(){
console.log('promise2')
})
})
setTimeout(function(){
console.log(4)
Promise.resolve(1).then(function(){
console.log('promise3')
})
})
// node中 每个阶段切换中间执行微任务
1
2
3
4
promise1
promise2
promise3
// 浏览器中 先走微任务
1
VM59:3 2
VM59:5 promise1
VM59:9 3
VM59:11 promise2
VM59:15 4
VM59:17 promise3
以下例子也可以看看
// 例子1
console.log(1);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise1')
setTimeout(function(){
console.log(3)
Promise.resolve(1).then(function(){
console.log('promise2')
})
})
})
})
// node
1
2
promise1
3
promise2
// 浏览器
1
VM70:3 2
VM70:5 promise1
VM70:8 3
VM70:10 promise2
// 例子2
console.log(11);
setTimeout(function(){
console.log(2);
Promise.resolve(1).then(function(){
console.log('promise1')
})
setTimeout(function(){
console.log(3)
Promise.resolve(1).then(function(){
console.log('promise2')
})
})
})
// node
11
2
promise1
3
promise2
// 浏览器
11
VM73:4 2
VM73:6 promise1
VM73:9 3
VM73:11 promise2
原文发布时间为:2018年06月04日
原文作者:我是家碧
本文来源: 掘金 如需转载请联系原作者