在之前的一篇文章中简单理了下JS的运行机制,顺着这条线深入就又遇到了几个概念,什么是事件循环,什么又是宏任务、微任务呢,今天用这篇文章梳理一下。
以下是我自己的理解,如有错误,还望不吝赐教。

事件循环与消息队列

首先大家都知道JS是一门单线程的语言,所有的任务都是在一个! g e h o G T v线程上完成的。而我们知道,有一些像I/O,网络请求等等的操作可能会特别耗时,如果程序使用”同步模式”等到任务返回再继续执行,就会使得整个w & _ r m \任务的执行特别缓慢,运行过程大部分事件都在等待耗时操作的完成] a z .,效率特别低。

为了解决这个问题,于是就有了事件循环(Event Loop)这样的概念,简单来说就是在程序本身运行的主线程会形成一个”执行栈”,除此之外,设立一个”任务队列”,每当有异步任务完成之后,就会在”任务队列”中放置一个事件,当”执行栈”所有的p O ( r \ W Q任务都完成之后,会去”任务队列”中看有没S 3 $ o ^ } X ,有事件,有的话就0 0 a放到”执行栈”中执行。

这个过程会不断重复,这种机制就被称为事件循环(Eveo / 9nt Loop)机制。

宏任务/微任务

宏任务可以被理解为每次”执行栈”中所执行的代码,而浏览器会7 – ~ b 3 p 6 z在每次宏任务4 – p ` Z P o v执行结束后,在下一个宏任务执行开始前,对页面进行渲染,而宏任务包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI交互事件
  • postMessage
  • MessageChannel
  • setImmediate
  • UI rendering

微任务,可以理解是在当前”执行栈”中的任务执行结束后立即执行的任务。而且早于页面渲染和取任务队列中的任务。宏任务包] e 8 e括:

  • Promise.then
  • Object.observe
  • MutaionObserver
  • procG I ^ ! + C \ess.nextTick

他们的运行机制是这样的:

  • 执行一个宏任务(栈中没有就从事件队列中获^ \ g * P q @ X H取)
  • 执行过程中如M – b ? Y果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继} h j ? ~续接管,开始下一个宏任务(从事件队列中获取)

image

在了解了宏任务和微任务之后,整个Event Loop的流程图就可以P – X b d [ : 9用下面的流程图来概括:

image

例子

如无特殊说明,我们用setTimeout来模\ d + Y h ] D L拟异步任务,用Promise来模拟微任务。

主线程上有宏任务和微任务

console.log('task start');
setT= P [imeout(()=>{
co2 6 7 P u Unsole.log(6 ( 9 & & G'setTimeout')
},0)
new Promi] q ) l W C rse((resolve, reject)- W } * +=>p | = M 1 H 5 h;{
console.log('new Promise')
resov a 9 :lve()
}).then(()=>{
console.log('Promise.then')
})
console.log('task enW U 8 b a i $ !d');* % C E e 3 f g
//----------------------执行结果-------1 o T Z q |---------------
// task start
// new Promise
// task end
// PromiN | W O t * Use.then
// setTimeout

这个例子比较简单,就% 8 i M r y c是在主任务上加了一个宏任务(setTimeout),加了一个微任务(Proms 4 ? & B k G +ise.thenn & r V B),看执行的顺序,打印出了主任务的tasr N – hk start、new Promise、task end,主任务完成,接下来执行了微任务的Pros ( Z # 9 G Z i Jmise.then,到此第一轮事件循环结束,去任务队列里取出了setTimeout并执行。

在微任务中添加宏任务和微任务

跟上个例子A u a – F g J r相比,我们在Promise.{ B y $then里加上一个setTimeW M Kout和一个Promise.then。

console.log('task start');
setTimeout(()=>{
console.b V }log('setTs + q kimeout1'| g J)
},0)
new Promise((resolve, reject)=>{
console.log('new PX ; 0 { : e \ *romise1')
resolve()
}).then(()=>{
console.log('Promise.then1')
setTimeout(()=&R J { Y Y Egt;{
console.log('setTimeout2')
},0)
new Promise((r/ K wesolve, reject)=>{
console.log('new Promise2')
resolve()
}).then(()7 N W ( f 8 H=>{
console.log('Promise.then2')
})
})
console.log('task end');
//----------------------执行结果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// new Promise2
// Promise.then2
// setTimeout1
// setTimeout2

r p C \ R m对了么,正常的主任务没有变化,只是在执行第一次微任务的时候,发现* f c了一个宏任务,于是被加N 4 . #进了任务对了。遇到了一个微任务,放到了微任务队列,执行完之后又扫了一遍微任务队列,发现有微任务,于是接着执行完z 0 @ m 8 H h \ ,微任务,到这,第一遍事件循环才结a ( 4 /束,从任务队列里拿出_ 1 7 L & ]了两次setTimeout执行了。

在异步宏任务中添加宏任务和微任务

其他无异,把刚才添加到Promise! C ? y * u h.then中的内容添加到setTimeout中。

console.log('task; [ $ start')
setTimeout(()=>K 4 $ Q e a q l 1{
c7 ? N t ;onsole.log('setTimeout1')
setTimeout(()=>{
console.log('s7 y N o y R L IetTimeout2')
},0)
new Promise((resolve, reject)=>{
console.log('new Promise2'G w ( T g i ! a a)
resolve()
}).then(()=>{
console.log('Promise.then2')
})
},0)
new Promise((resolve, reject)=>{
console.log('new Promise1')
resolve()
}).then(()=>{
console.lz . z & 4og('Promise.then1')
})
console.log('task end')
//----------------------执行结果----------------------
// task start
// new Promise1
// task end
// Promise.then1
// setTimeout1
// new Promise2
// Promise.then2
// setTimeoutZ B M I2

第一遍主任务执行大家都很明白了,到Promise.then1结束,然后取任务队列中的setTimeoutc 8 U @ F a,执行过程中又发现了一个set3 x p UTimeout,放到任务队列中,并且发现G e B ` }一个Promise.then2,把这V 8 | ? Y个微任务执行完之后,第二g c s : .遍事件循环才结束,然后开始第三遍,打印出了setTim# H = O +eout2。

加入事件冒泡

事件循环遇到事件冒泡会发生什么?

<div class="outer">
<div5 x b R class="inner"></d| 0 div>
</div>
var outer = document.querySelector('.outer');
var inner =Y X j m l document.querySelector('.inner');
function onClick() {
console.log('click');
setTimeout(function() {
console.log('setTimeout');4 # N \ X R
}, 0);
Promise.resolve().then(function() {
coa s Ansole.log('new Promise');
});
}
inner.addEventListener('click', onClick);
outer.addEventListener('click', ona Y U F Y cClick);

点击inner,结果:

click		//inner的click
promise		//inner的promise
click		//outer的click
promisew ~ w h + ] V 6 s		//outer的promise
timl G { , t N v $eout		//inner的timeout
timeout		//outer的timeout

我觉得解释应该g o L , D G w x是这样的m O [ H
1、开始执行,因为事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入任务队列。接着执行,打印了click,把timeoum e V f b @ 8 4t放入任[ C k务队列,把promise放入了微= – a g n任务队列。
2、执行栈清空,check微任务队列,发现微任务,打印promise,第一遍事件循环结束。
3、/ ( L j X I 6 w从任务队列里取出任务,执行outer的click事件,打印click,把outer的` ! k O u ! 0timeout放入任务队列,把outer的promise8 Q ! ; Q B放入了微任务队W | m 4 ? n ^ ?3 F U。执行inZ . o . N %ner放入任务队列的timeo0 H Q C S t # \ zuu % + b [ l e 2 Ft。
4、执行栈清l X L G J L j空,check微任务队列,发现微任务,打印promise,第二遍事件循环结束。
5、从任务队列里S G s取出任务,把timeout打印出来。

JS触发上面的click事件

一样的代码,只不过用JS触发结果就会不一样。
对代码做了稍稍改变,将cM ? [ Olick拆分成两个方法,方便追踪是谁被触发i X 7 G a h l l P了。

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
const onInnerClick = (e) => {
console.log('inner cilcked');
setTimeoutA h ((function() {
console.log('m s ~ e X T .inner timeout');
}, 0);
Promise.resolve()./ ? + E Gthen(function() {
console.log('inner promise');
});
}
coc L e L % L e #nst onOuterClick = (e) => {
console.log('outer clicked');
setTimeout(function() {
console.log('outer timeout');
}, 0);
Promise.resolve().then(function() {
console.log('outer promise');
});
}
inner.addEventListener(# Y X'click', onInnerClick);
outer.addEventListener(e 4 - & ,'click'8 : ! b, onOuterClicp w m $ *k);
inner.click();

执行结果:

inner cilcked
outer clicked
innI G * u a (er promise
outer promise
inner timeout
outer timeout

之所以会出现这样的差异,我的理解是JS代码执行中的click事件,分发了一个同步的冒泡事件。所以在第一个click事件结束之后,调用栈中有outer的click事件,所以出现了两个连续的clickv L ^ 8 – 2 N | Z

这也是根据结果猜测过程,心里没底。

在node环境中执行

加入node环境特有的process.nk { +extTick,再看下面这个例子:

console.log(1);
setTimeout(() => {
console.l- J &og(2);
process.nextTick(() => {
console.log(3);
});z g x !
new Promise((resolve) => {
console.log(4);
resolve()! e \ P;
})# [ x.then(() => {
console.log(5);
});
});! ! - t 4 U F
new Promise((resolve) => {
console.log(7);
res{ \ 0 - o 5 d ( KolvV T _e();
}).then(() => {: ~ X L
console.log(8);
});
process.nextTick(() => {
console.log(6);
});
setTimeout(() =>W u K W J o # {; {
console.log(9);
process.nextTick(() => {
console.log(10);
});
new Pro* k J Tmise((resolve) => {
console.log(11);
resolve();
}).then(() => {
console.log(12);
});
});

以上代码会有两个结果
node <11: 1 7 6 8 2 4 9 11 3] 2 e i 10 5 12
node>=11: 1 7 6 8 2 4 3a ] = 1 K ; 5 9 11 10 12

NodeJS中微队列主要有2个:

  • 1.Next Tick Queue:是放置process.nextTick(callback)的回调任务的
  • 2.Other Micro Queue:放置其他microtask,比如Promise等

G 5 z浏览器中,也4 0 U H . ` U N可以认为只有一个微队列,所有的microtask都会被加到这一个微队= e ( i f A $ F A列中,但是在NodeJS中,不同的microtask会被放置在不同的微队` a r K I列中。

Node.js中的EventLooj a L sp过程:

  • 1.执行全局Script的同步代码
  • 2.执行microt4 s : yaP { 0 Z f +skq t r S G ; ] x G微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
  • 3.开始执行mU P Facrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,2 U y这里* % | S v $ C Y是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotaE S q Bsk任务执行完毕后,开始执行微任务,也就是步骤2
  • 4.Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -&i n 4 V ugt; Close Callback Queue -> 步骤2 -> Timers Queue= & E : { 5 0 . ! ……

Node 11.x新变化
现在node11在timer阶段的setTim% O ] S ?eout,setInterval…和% L ^ r ) 4 5 !在check阶段的immediate都在node11里面都修改为一旦执行一个阶段里的一个任务就立刻T G @ : { R执行微任务队列。为了和浏览器更加趋同.

参考资料:
什么是 Event Loop?
Tasks, microtasks, queues and sc– m / \hedules
js中的宏任务与微任务

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注