Event loop

在上图中可以看到,setTimeout这类异步接口实际上不在JS引擎中,而是由浏览器中的Web(图中的V8是chrome中的JS引擎,safari、firefox则是各自的引擎,参考《主流浏览器内核及JS引擎》)
setTimeout(() => console.log(1) }, 0) console.log(0)
执行的过程:
-
然后「JS引擎线程」执行了console.log(0),控制台中输出了0
-
浏览器中的「定时器线程」接管该定时任务,当定时任务超时后,将回调函数即console.log(1)塞入到宏任务队列中,等待调度,如图中「2」
-
「event loop线程」检查JS执行栈是否为空,如果是,则将宏任务队列中的任务塞入JS执行栈中,如图中「8」
-
「JS引擎线程」处理执行栈中的console.log(1)代码,控制台中输出1
从这个流程可以看出,无论超时设置的多短,setTimeout中的回调函数必须等到下一次事件循环才能被处理,这就不难理解为什么0会在1之前输出了
promise.then函数
Promise.resolve().then(()=>{ console.log(2) }) console.log(0)
执行的过程:
-
「JS引擎线程」运行JS执行栈中执行如上代码,由于Promise首先执行了resolve函数变为终结状态,因此立即执行promise.then方法,此方法实际上是创建了一个微任务,并将console.log(2)塞入微任务队列中,如图「5」所示。
-
然后「JS引擎线程」执行了console.log(0),控制台中输出了0
-
「event loop线程」检查JS执行栈是否为空,如果是,则将微任务队列中的任务塞入JS执行栈中,如图中「8」
-
「JS引擎线程」处理执行栈中的console.log(2)代码,控制台中输出2
任务分类
JS事件循环中的任务分为两大类
-
宏任务,即task
-
微任务,即microTask,也称job
对于浏览器环境和Node环境,提供的接口会有些许差别
宏任务接口
| 接口 | 浏览器 | Node |
|---|---|---|
| I/O操作 | ✅ | ✅ |
| setTimeout | ✅ | ✅ |
| setInterval | ✅ | ✅ |
| setImmediate | ❌ | ✅ |
| requestAnimationFrame | ✅ | ❌ |
微任务接口
| 接口 | 浏览器 | Node |
|---|---|---|
| process.nextTick | ❌ | ✅ |
| promise.then/catch/finally | ✅ | ✅ |
| MutationObserver | ✅ | ❌ |
任务优先级
-
整体上,微任务的处理优先于宏任务。准确的说是微任务会在本此事件循环结束前处理,宏任务会在下次事件循环开始时处理。
-
在Node环境中,process.nextTick产生的微任务优先级最高,会被插到微任务队列的最前端优先处理;而setImmediate产生的宏任务优先级最高,会在所有宏任务之前执行
链式调用
通过上面的例子,Promise的基本执行顺序应该能清楚了,下面再看一个更复杂的问题,即当Promise.then链式调用时,代码是如何运行的。
问题来源于这片文章:《从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节》
Promise.resolve().then(() => { console.log(0); return Promise.resolve(4); }).then((res) => { console.log(res) }) Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(5); })
在看这个复杂的问题前,先看两个关于Promise.resolve和Promise.then的简单例子
Promise.resolve()
Promise.resolve是一个静态方法,它实际上是创建了一个立即resolve的Promise对象,以下两段代码是等同的
Promise.resolve(0) // 等同于 new Promise((resolve, reject) => { resolve(0) })
所以,
Promise.resolve(0).then(data => console.log(data))
这段代码的执行过程:
-
创建一个Promise对象,立即将其状态改变为fulfilled
-
执行promise.then方法,由于此时的promise状态是fulfilled,会直接将其后的成功回调函数塞入微任务队列,等待调度
Promise.then()
我们知道,then方法会返回一个新的Promise对象,从而提供了链式调用的特性。来看这个例子:
Promise.resolve(0).then(data => { console.log(data) return 1 }).then(data => { console.log(data) })
我们省略细节,简单描述这个事件循环的过程
-
第一个then创建了一个微任务
-
执行第一个微任务,控制台输出0,并且返回1,返回的1将作为下一次then的成功回调函数的入参
-
第二个then创建一个新的微任务
-
执行第二个微任务,控制台输出1
可以看到,当then返回一个普通对象时,会简单的将此对象作为入参交给下一次then调用。但是,如果当then返回一个Promise对象时会发生什么?
Promise.resolve(0).then(data => { console.log(data) return Promise.resolve(1) }).then(data => { console.log(data) })
在第一个then中,我们返回了一个fulfilled状态的Promise对象,如果还是按照普通对象的处理逻辑,直接把该Promise对象作为入参交给下一个then函数,那么上述代码的输出会变成
0
Promise {<fulfilled>: 1}
显然,这不是我们预期的结果。我们希望的是将Promise.resolve(1)执行完毕后,将其resolve的结果(例子里就是1)交给下一个then来使用。所以,实际当then方法中返回一个Promise对象时,会先执行这个Promise对象,然后再处理其后的then方法。为了理解这个机制,可以简单的理解为,在两个then函数中,插入了一个then函数
// 简化理解代码执行顺序,实际处理过程中会多塞一次微任务队列,后面会谈到 Promise.resolve(0).then(data => { console.log(data) }).then(data => { return 1 }).then(data => { console.log(data) })
例子中代码的执行结果是
0
1
实际上,为了处理then方法中返回的Promise对象,机制要比上面的简化写法复杂。
-
V8首先创建了一个 NewPromiseResolveThenableJobTask类型的微任务,然后将此任务放入微任务队列中等待处理(微任务+1)
-
当上面的微任务被调度,处理NewPromiseResolveThenableJobTask时,实际就是调用了Promise.then方法,此时又创建了一个微任务(微任务+1)
-
所以,实际上需要两次微任务才能把这个Promise对象转变为fulfilled状态
综合分析
有了以上基础,我们再回到那个复杂的问题
// PromiseA Promise.resolve().then(() => { console.log(0); return Promise.resolve(4); }).then((res) => { console.log(res) }) // PromiseB Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(5); })
// 1.创建一个状态为fulfilled的Promise const PromiseA = Promise.resolve() // 2.由于PromiseA的状态为fulfilled,其后then函数中的成功回调会直接放入微任务中等待处理,PromiseA1的状态为pending const PromiseA1 = PromiseA.then(() => { console.log(0); return Promise.resolve(4); }) // 3.由于PromiseA1的状态为pending,其后then函数会被放入成功回调队列中,等待PromiseA1的状态变为fulfilled时再处理 const PromiseA2 = PromiseA1.then((res) => { console.log(res) }) // 4.创建一个状态为fulfilled的Promise const PromiseB = Promise.resolve() // 5.由于PromiseB的状态为fulfilled,其后then函数中的成功回调会直接放入微任务中等待处理,PromiseB1的状态为pending const PromiseB1 = PromiseB.then(() => { console.log(1); }) // 6.由于PromiseB1的状态为pending,其后then函数会被放入成功回调队列中,等待PromiseB1的状态变为fulfilled时再处理 const PromiseB2 = PromiseB1.then(() => { console.log(2); }) // 7.同上,放入PromiseB2的成功回调队列中 const PromiseB3 = PromiseB2.then(() => { console.log(3); }) // 8.同上,放入PromiseB3的成功回调队列中 const PromiseB4 = PromiseB3.then(() => { console.log(5); })
上述代码会先按顺序同步执行,可以看到,此时的微任务队列中只有第2步和第5步创建的两个微任务任务
这两个任务按顺序依次执行,产生的结果为
-
控制台依次输出0, 1
-
由于PromiseA的成功回调中返回了一个Promise对象,根据我们之前的讨论,此操作需要两轮微任务才能将此PromiseA1的状态改变为fulfilled,所以PromiseA2需要两轮后才能执行
-
PromiseB1状态变为fulfilled,其后的then方法的成功回调会继续放入微任务中处理
所以,控制台的输出结果是
0
1
2
3
4
5
浙公网安备 33010602011771号