众所周知,JS 是单线程语言,事件执行只能依次进行。但是 JS 的事件有时会发生延迟执行,例如 setTimeout、Promise 之类的方法。那么,我们该如何判断事件执行的先后顺序呢?
1. 同步事件和异步事件
对于 JS 事件,我们可以分为 同步(Synchronous)和 异步(Asynchronous)两种事件类型,执行顺序:先同步再异步。
同步事件例如:console.log(1)、new Promise((resolve, reject) => { // ... }) 等
异步事件例如:setTimeout、setInterval、Promise.resolve().then(() => { // ... }) 等
JS 事件循环(Event Loop),顾名思义,存在一个事件循环机制,在每一次 JS 事件循环的时候,总是先执行同步,再执行异步,这句话可能有点模糊,先来看代码:
// 例1:
console.log(1) // 同步事件
setTimeout(function() { // 异步事件
console.log(2)
}, 0) /* 即使延迟为 0 它也是异步事件 */
new Promise((resolve, reject) => { // 同步事件
console.log(3)
})
console.log(4) // 同步事件
上述代码执行结果为: 1 -> 3 -> 4 -> 2
解释:如之前所述,先同步再异步。(其实这里有两层事件循环,具体请往下看)
2.事件队列(Event Queues)
什么是事件队列?你可以理解为,在 JS 执行事件循环的时候,会将所有的异步事件放入事件队列中去,所以它就相当于是一个包裹着待执行的异步事件的容器。
不理解?我们来对上面那个例1进行分析:
① console.log(1),判断为同步,立即执行,输出 1;
② setTimeout,判断为异步,放入事件队列(event queues)中去;
③ new Promise,判断为同步,立即执行,输出 3;
④ console.log(4),判断为同步,立即执行,输出 4。
此为第一轮事件结束,在确认没有同步事件后,接下来进入第二轮事件,此时的事件队列中有个异步事件,分析得知,该事件中有一个同步事件 console.log(2),ok,立即执行,事件循环结束。为了加深理解,来个例子:
// 例2
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
new Promise((resolve, reject) => {
console.log(3)
setTimeout(() => {
console.log(4)
resolve()
}, 0)
}).then(() => {
console.log(5)
})
console.log(6)
上述代码,你可能还是有些困惑,我们先来分析一波(注意:本着这样一个原则,遇到同步,立即执行;遇到异步,放入队列,等待执行):
先第一轮事件,Event Loop 1:
① console.log(1),同步,立即执行,结果 1;
② setTimeout,异步,放入队列,记为队列1,等待执行;
③ new Promise() 中的 console.log(3),同步,立即执行,结果 3,这里还有个 setTimeout,所以也列为异步,放入队列,记为队列2,等待执行;
④ new Promise.then(),异步,放入队列,记为队列3,等待执行;
⑤ console.log(6),同步,立即执行,结果 6
此时,第一轮时间分析结束
第二轮事件分析,此时的事件队列中有三个,Event Loop 2:
① 队列1中有一个 console.log(2),同步,立即执行,结果 2;
② 队列2中有一个 console.log(4),同步,立即执行,结果 4;
③ 队列3中有一个 console.log(5),同步,立即执行,结果 5.
至此,事件循环结束(没有事件队列存在)。所以结果为:1 -> 3 -> 6 -> 2 -> 4 -> 5
3.宏任务(macrotask)和微任务(microtask)
说完了上面的任务队列,再说说事件机制中的宏任务和微任务。
宏任务和微任务都属于异步任务,换句话说,异步队列中的任务有两种类型,即宏任务和微任务
哪些是宏任务,哪些是微任务呢?
setTimeout、setInterval、setImmediate等这些常见的属于宏任务;
Promise.then()、process.nextTick(类似node.js版的"setTimeout")等这些常见的属于微任务
注意:在一个队列中,总是先执行所有微任务,再执行宏任务。上代码:
// 例3
console.log(1) // --------------------Event Loop 1: 同步1-1
setTimeout(() => { // ----------------Event Loop 1: 异步,队列1-1,(宏)-----Event Loop 2: 队列2-1
console.log(2)
}, 0)
process.nextTick(() => { // ----------Event Loop 1: 异步,队列1-2,(微)
console.log(3)
})
new Promise((resolve, reject) => { //-Event Loop 1: 同步1-2
console.log(4)
resolve()
}).then(() => { // -------------------Event Loop 1: 异步,队列1-3,(微)
console.log(5)
})
console.log(6) // --------------------Event Loop 1: 同步1-3
老规矩,先来分析一波再说(同步不再详说,相信你已经完全理解了,主要看异步中的宏任务和微任务):
第一轮事件分析,Event Loop 1:
① 先同步,分别是同步1-1/1-2/1-3,结果 1 -> 4 -> 6 ;
② 再看异步,此时队列中有三个队列,队列1-1/1-2/1-3,先知行微任务,在执行宏任务;
③ 队列中的微任务为:队列1-2和1-3,所以结果 3 -> 5;
④ 队列中的宏任务:队列1-1,丢入下一个队列2-x中
第二轮事件分析,Event Loop 2:
① 队列2-x中,只有一个队列2-1,结果 2;
至此,事件循环结束,例3的结果为:1 -> 4 -> 6 -> 3 -> 5 -> 2
通过上述三个例子,你是否已经或者大致了解了这些基本概念呢?再来一个例子,加深巩固一下:
// 例4
console.log(1) // ---------------------Event Loop 1: 同步1-1
setTimeout(()=> { // ------------------Event Loop 1: 异步,队列1-1,(宏)-----Event Loop 2: 队列2-1
console.log(2) // -------------------------------------------------------Event Loop 2: 同步2-1-1
process.nextTick(() => { // ---------------------------------------------Event Loop 2: 异步,队列2-1-1,(微)
console.log(3)
})
new Promise((resolve, reject) => { // -----------------------------------Event Loop 2: 同步2-1-2
console.log(4)
resolve()
}).then(() => { // ------------------------------------------------------Event Loop 2: 异步,队列2-1-2,(微)
console.log(5)
})
}, 0)
process.nextTick(() => { // ----------Event Loop 1: 异步,队列1-2,(微)
console.log(6)
})
new Promise((resolve, reject) => { //--Event Loop 1: 同步1-2
console.log(7)
resolve()
}).then(() => { // -------------------Event Loop 1: 异步,队列1-3,(微)
console.log(8)
})
setTimeout(() => { // ----------------Event Loop 1: 异步,队列1-4,(宏)-----Event Loop 2: 队列2-2
console.log(9) // ------------------------------------------------------Event Loop 2: 同步2-2-1
process.nextTick(() => { // --------------------------------------------Event Loop 2: 异步,队列2-2-1,(微)
console.log(10)
})
new Promise((resolve, reject) => { // ----------------------------------Event Loop 2: 同步2-2-2
console.log(11)
resolve()
}).then(() => { // -----------------------------------------------------Event Loop 2: 异步,队列2-2-2,(微)
console.log(12)
})
})
console.log(13) // ------------------Event Loop 1: 同步1-3
看起来很复杂,为了加深理解,继续分析:
第一轮事件分析,Event Loop 1:
① 先同步,分别是同步1-1/1-2/1-3,结果 1 -> 7 -> 13 ;
② 再看异步,此时队列中有四个队列,队列1-1/1-2/1-3/1-4,先知行微任务,在执行宏任务;
③ 队列中的微任务为:队列1-2和1-3,所以结果 6 -> 8;
④ 队列中的宏任务:队列1-1和1-4,丢入下一个队列2-x中
第二轮事件分析,Event Loop 2:
① 队列2-1中:先同步,分别是同步2-1-1/2-1-2,结果 2 -> 4;
② 队列2-1中:再看异步,此时有两个队列,队列2-1-1/2-1-2,且都是微任务,那就按顺序执行,结果 3 -> 5。此时队列2-1执行结束。
③ 队列2-2中:先同步,分别是同步2-2-1/2-2-2,结果 9 -> 11;
④ 队列2-2中:再看异步,此时有两个队列,队列2-2-1/2-2-2,且都是微任务,那就按顺序执行,结果 10 -> 12。此时队列2-2执行结束。
至此,事件循环结束,结果是:1 -> 7 -> 13 -> 6 -> 8 -> 2 -> 4 -> 3 -> 5 -> 9 -> 11 -> 10 -> 12。
emmmmm......大致就是这么回事。
如果理解有误,还请大神指点,THKS:)