有关 Eventloop+Promise 的面试题大约分以下几个版本 -- 得心应手版, 游刃有余版, 炉火纯青版, 登峰造极版和究极变态版. 假设小伙伴们战到最后一题, 以后遇到此类问题, 都是所向披靡. 当然如果面试官们还能想出更变态的版本, 算我输.
版本一: 得心应手版
考点: eventloop 中的执行顺序, 宏任务微任务的区别.
吐槽: 这个不懂, 没得救了, 回家重新学习吧.
- setTimeout(()=>{
- console.log(1)
- },0)
- Promise.resolve().then(()=>{
- console.log(2)
- })
- console.log(3)
这个版本的面试官们就特别友善, 仅仅考你一个概念理解, 了解宏任务 (marcotask) 微任务(microtask), 这题就是送分题.
笔者答案: 这个是属于 Eventloop 的问题. main script 运行结束后, 会有微任务队列和宏任务队列. 微任务先执行, 之后是宏任务.
版本二: 游刃有余版
这一个版本, 面试官们为了考验一下对于 Promise 的理解, 会给题目加点料:
考点: Promise 的 executor 以及 then 的执行方式
吐槽: 这是个小坑, promise 掌握的熟练的, 这就是人生的小插曲.
- setTimeout(()=>{
- console.log(1)
- },0)
- let a=new Promise((resolve)=>{
- console.log(2)
- resolve()
- }).then(()=>{
- console.log(3)
- }).then(()=>{
- console.log(4)
- })
- console.log(4)
此题看似在考 Eventloop, 实则考的是对于 Promise 的掌握程度. Promise 的 then 是微任务大家都懂, 但是这个 then 的执行方式是如何的呢, 以及 Promise 的 executor 是异步的还是同步的?
错误示范: Promise 的 then 是一个异步的过程, 每个 then 执行完毕之后, 就是一个新的循环的, 所以第二个 then 会在 setTimeout 之后执行.(没错, 这就是某年某月某日笔者的一个回答. 请给我一把枪, 真想打死当时的自己.)
正确示范: 这个要从 Promise 的实现来说, Promise 的 executor 是一个同步函数, 即非异步, 立即执行的一个函数, 因此他应该是和当前的任务一起执行的. 而 Promise 的链式调用 then, 每次都会在内部生成一个新的 Promise, 然后执行 then, 在执行的过程中不断向微任务 (microtask) 推入新的函数, 因此直至微任务 (microtask) 的队列清空后才会执行下一波的 macrotask.
详细解析
(如果大家不嫌弃, 可以参考我的另一篇文章, 从零实现一个 Promise, 里面的解释浅显易懂.) 我们以 babel 的 core-JS 中的 promise 实现为例, 看一眼 promise 的执行规范:
代码位置: promise-polyfill
- PromiseConstructor = function Promise(executor) {
- //...
- try {
- executor(bind(internalResolve, this, state), bind(internalReject, this, state));
- } catch (err) {
- internalReject(this, state, err);
- }
- };
这里可以很清除地看到 Promise 中的 executor 是一个立即执行的函数.
- then: function then(onFulfilled, onRejected) {
- var state = getInternalPromiseState(this);
- var reaction = newPromiseCapability(speciesConstructor(this, PromiseConstructor));
- reaction.ok = typeof onFulfilled == 'function' ? onFulfilled : true;
- reaction.fail = typeof onRejected == 'function' && onRejected;
- reaction.domain = IS_NODE ? process.domain : undefined;
- state.parent = true;
- state.reactions.push(reaction);
- if (state.state != PENDING) notify(this, state, false);
- return reaction.promise;
- },
接着是 Promise 的 then 函数, 很清晰地看到 reaction.promise, 也就是每次 then 执行完毕后会返回一个新的 Promise. 也就是当前的微任务 (microtask) 队列清空了, 但是之后又开始添加了, 直至微任务 (microtask) 队列清空才会执行下一波宏任务(marcotask).
- //state.reactions 就是每次 then 传入的函数
- var chain = state.reactions;
- microtask(function () {
- var value = state.value;
- var ok = state.state == FULFILLED;
- var i = 0;
- var run = function (reaction) {
- //...
- };
- while (chain.length> i) run(chain[i++]);
- //...
- });
最后是 Promise 的任务 resolve 之后, 开始执行 then, 可以看到此时会批量执行 then 中的函数, 而且还给这些 then 中回调函数放入了一个 microtask 这个很显眼的函数之中, 表示这些回调函数是在微任务中执行的.
那么在没有 Promise 的浏览器中, 微任务这个队列是如何实现的呢?
小知识: babel 中对于微任务的 polyfill, 如果是拥有 setImmediate 函数平台, 则使用之, 若没有则自定义则利用各种比如 Node.JS 中的 process.nextTick, 浏览器中支持 postMessage 的, 或者是通过 create 一个 script 来实现微任务(microtask). 最终的最终, 是使用 setTimeout, 不过这个就和微任务无关了, promise 变成了宏任务的一员.
拓展思考:
为什么有时候, then 中的函数是一个数组? 有时候就是一个函数?
我们稍稍修改一下上述题目, 将链式调用的函数, 变成下方的, 分别调用 then. 且不说这和链式调用之间的不同用法, 这边只从实践角度辨别两者的不同. 链式调用是每次都生成一个新的 Promise, 也就是说每个 then 中回调方法属于一个 microtask, 而这种分别调用, 会将 then 中的回调函数 push 到一个数组之中, 然后批量执行. 再换句话说, 链式调用可能会被 Evenloop 中其他的函数插队, 而分别调用则不会(仅针对最普通的情况, then 中无其他异步操作.).
- let a=new Promise((resolve)=>{
- console.log(2)
- resolve()
- })
- a.then(()=>{
- console.log(3)
- })
- a.then(()=>{
- console.log(4)
- })
下一模块会对此微任务 (microtask) 中的 "插队" 行为进行详解.
版本三: 炉火纯青版
这一个版本是上一个版本的进化版本, 上一个版本的 promise 的 then 函数并未返回一个 promise, 如果在 promise 的 then 中创建一个 promise, 那么结果该如何呢?
考点: promise 的进阶用法, 对于 then 中 return 一个 promise 的掌握
吐槽: promise 也可以是地狱......
- 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")
- })
按照上一节最后一个 microtask 的实现过程, 也就是说一个 Promise 所有的 then 的回调函数是在一个 microtask 函数中执行的, 但是每一个回调函数的执行, 又按照情况分为立即执行, 微任务 (microtask) 和宏任务(macrotask).
遇到这种嵌套式的 Promise 不要慌, 首先要心中有一个队列, 能够将这些函数放到相对应的队列之中.
Ready GO
第一轮
current task: promise1 是当之无愧的立即执行的一个函数, 参考上一章节的 executor, 立即执行输出[promise1]
micro task queue: [promise1 的第一个 then]
第二轮
current task: then1 执行中, 立即输出了 then11 以及新 promise2 的 promise2
micro task queue: [新 promise2 的 then 函数, 以及 promise1 的第二个 then 函数]
第三轮
current task: 新 promise2 的 then 函数输出 then21 和 promise1 的第二个 then 函数输出 then12.
micro task queue: [新 promise2 的第二 then 函数]
第四轮
current task: 新 promise2 的第二 then 函数输出 then23
- micro task queue: []
- END
最终结果[promise1,then11,promise2,then21,then12,then23].
变异版本 1: 如果说这边的 Promise 中 then 返回一个 Promise 呢??
- 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 的状况了, 这个考的重点在于 Promise 而非 Eventloop 了. 这里就很好理解为何 then12 会在 then23 之后执行, 这里 Promise 的第二个 then 相当于是挂在新 Promise 的最后一个 then 的返回值上.
变异版本 2: 如果说这边不止一个 Promise 呢, 再加一个 new Promise 是否会影响结果??
- 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")
- })
笑容逐渐变态, 同样这个我们可以自己心中排一个队列:
第一轮
- current task: promise1,promise3
- micro task queue: [
promise2 的第一个 then
,
promise3 的第一个 then
]
第二轮
- current task: then11,promise2,then31
- micro task queue: [
promise2 的第一个 then
,
promise1 的第二个 then
]
第三轮
- current task: then21,then12
- micro task queue: [
promise2 的第二个 then
]
第四轮
- current task: then23
- micro task queue: []
最终输出:[promise1,promise3,then11,promise2,then31,then21,then12,then23]
版本四: 登峰造极版
考点: 在 async/await 之下, 对 Eventloop 的影响.
槽点: 别被 async/await 给骗了, 这题不难.
相信大家也看到过此类的题目, 我这里有个相当简易的解释, 不知大家是否有兴趣.
- 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');
async/await 仅仅影响的是函数内的执行, 而不会影响到函数体外的执行顺序. 也就是说 async1()并不会阻塞后续程序的执行, await async2()相当于一个 Promise,console.log("async1 end"); 相当于前方 Promise 的 then 之后执行的函数.
按照上章节的解法, 最终输出结果:[script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout]
如果了解 async/await 的用法, 则并不会觉得这题是困难的, 但若是不了解或者一知半解, 那么这题就是灾难啊.
此处唯一有争议的就是 async 的 then 和 promise 的 then 的优先级的问题, 请看下方详解.*
async/await 与 promise 的优先级详解
- async function async1() {
- console.log("async1 start");
- await async2();
- console.log("async1 end");
- }
- async function async2() {
- console.log( 'async2');
- }
- // 用于 test 的 promise, 看看 await 究竟在何时执行
- new Promise(function (resolve) {
- console.log("promise1");
- resolve();
- }).then(function () {
- console.log("promise2");
- }).then(function () {
- console.log("promise3");
- }).then(function () {
- console.log("promise4");
- }).then(function () {
- console.log("promise5");
- });
先给大家出个题, 如果让你 polyfill 一下 async/await, 大家会怎么 polyfill 上述代码? 下方先给出笔者的版本:
- function promise1(){
- return new Promise((resolve)=>{
- console.log("async1 start");
- promise2().then(()=>{
- console.log("async1 end");
- resolve()
- })
- })
- }
- function promise2(){
- return new Promise((resolve)=>{
- console.log( 'async2');
- resolve()
- })
- }
在笔者看来, async 本身是一个 Promise, 然后 await 肯定也跟着一个 Promise, 那么新建两个 function, 各自返回一个 Promise. 接着 function promise1 中需要等待 function promise2 中 Promise 完成后才执行, 那么就 then 一下咯~.
根据这个版本得出的结果:[async1 start,async2,promise1,async1 end,promise2,...],async 的 await 在 test 的 promise.then 之前, 其实也能够从笔者的 polifill 中得出这个结果.
然后让笔者惊讶的是用原生的 async/await, 得出的结果与上述 polyfill 不一致! 得出的结果是:[async1 start,async2,promise1,promise2,promise3,async1 end,...], 由于 promise.then 每次都是一轮新的 microtask, 所以 async 是在 2 轮 microtask 之后, 第三轮 microtask 才得以输出(关于 then 请看版本三的解释).
/* 突如其来的沉默 */
这里插播一条, async/await 因为要经过 3 轮的 microtask 才能完成 await, 被认为开销很大, 因此之后 V8 和 Nodejs12 开始对此进行了修复, 详情可以看 GitHub 上面这一条 pull https://github.com/tc39/ecma262/pull/1250
那么, 笔者换一种方式来 polyfill, 相信大家都已经充分了解 await 后面是一个 Promise, 但是假设这个 Promise 不是好 Promise 怎么办? 异步是好异步, Promise 不是好 Promise.V8 就很凶残, 加了额外两个 Promise 用于解决这个问题, 简化了下源码, 大概是下面这个样子:
- // 不太准确的一个描述
- function promise1(){
- console.log("async1 start");
- // 暗中存在的 promise, 笔者认为是为了保证 async 返回的是一个 promise
- const implicit_promise=Promise.resolve()
- // 包含了 await 的 promise, 这里直接执行 promise2, 为了保证 promise2 的 executor 是同步的感觉
- const promise=promise2()
- // https://tc39.github.io/ecma262/#sec-performpromisethen
- // 25.6.5.4.1
- // throwaway, 为了规范而存在的, 为了保证执行的 promise 是一个 promise
- const throwaway= Promise.resolve()
- //console.log(throwaway.then((d)=>{console.log(d)}))
- return implicit_promise.then(()=>{
- throwaway.then(()=>{
- promise.then(()=>{
- console.log('async1 end');
- })
- })
- })
- }
ps: 为了强行推迟两个 microtask 执行, 笔者也是煞费苦心.
总结一下: async/await 有时候会推迟两轮 microtask, 在第三轮 microtask 执行, 主要原因是浏览器对于此方法的一个解析, 由于为了解析一个 await, 要额外创建两个 promise, 因此消耗很大. 后来 V8 为了降低损耗, 所以剔除了一个 Promise, 并且减少了 2 轮 microtask, 所以现在最新版本的应该是 "零成本" 的一个异步.
版本五: 究极变态版
饕餮大餐, 什么变态的内容都往里面加, 想想就很丰盛. 能考到这份上, 只能说面试官人狠话也多.
考点: Node.JS 事件 + Promise+async/await + 佛系 setImmediate
槽点: 笔者都不知道那个可能先出现
- 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");
- });
- async1()
- new Promise(function (resolve) {
- console.log("promise1");
- resolve();
- }).then(function () {
- console.log("promise2");
- });
- setImmediate(()=>{
- console.log("setImmediate")
- })
- process.nextTick(()=>{
- console.log("process")
- })
- console.log('script end');
队列执行 start
第一轮:
- current task:"script start","async1 start",'async2',"promise1","script end"
- micro task queue:[async,promise.then,process]
- macro task queue:[setTimeout,setImmediate]
第二轮
- current task:process,async1 end ,promise.then
- micro task queue:[]
- macro task queue:[setTimeout,setImmediate]
第三轮
- current task:setTimeout,setImmediate
- micro task queue:[]
- macro task queue:[]
最终结果:[script start,async1 start,async2,promise1,script end,process,async1 end,promise2,setTimeout,setImmediate]
同样 "async1 end","promise2" 之间的优先级, 因平台而异.
笔者干货总结
在处理一段 evenloop 执行顺序的时候:
第一步确认宏任务, 微任务
宏任务: script,setTimeout,setImmediate,promise 中的 executor
微任务: promise.then,process.nextTick
第二步解析 "拦路虎", 出现 async/await 不要慌, 他们只在标记的函数中能够作威作福, 出了这个函数还是跟着大部队的潮流.
第三步, 根据 Promise 中 then 使用方式的不同做出不同的判断, 是链式还是分别调用.
最后一步记住一些特别事件
比如, process.nextTick 优先级高于 Promise.then
来源: https://juejin.im/post/5c9a43175188252d876e5903