有关 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 函数平台,则使用之,若没有则自定义则利用各种比如 nodejs 中的 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: [promise 的第一个 then]
第二轮
current task: then1 执行中,立即输出了 then11 以及新 promise 的 promise2
micro task queue: [新 promise 的 then 函数, 以及 promise 的第二个 then 函数]
第三轮
current task: 新 promise 的 then 函数输出 then21 和 promise 的第二个 then 函数输出 then12。
micro task queue: [新 promise 的第二 then 函数]
第四轮
current task: 新 promise 的第二 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
那么,笔者换一种方式来 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,所以现在最新版本的应该是“零成本”的一个异步。
版本五:究极变态版
饕餮大餐,什么变态的内容都往里面加,想想就很丰盛。能考到这份上,只能说面试官人狠话也多。
考点:nodejs 事件 +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
参考网址,推荐阅读:
有关 V8 中如何实现 async/await 的,更快的异步函数和 Promise
有关 async/await 规范的,ecma262
还有 babel-polyfill 的源码,promise
后记
Hello~Anybody here?
本来笔者是不想写这篇文章的,因为有种 5 年高考 3 年模拟的既视感,奈何面试官们都太凶残了,为了“折磨”面试者无所不用其极,怎么变态怎么来。不过因此笔者算是彻底掌握了 Eventloop 的用法,因祸得福吧~
有小伙伴看到最后嘛?来和笔者聊聊你遇到过的的 Eventloop+Promise 的变态题目。
欢迎转载~ 但请注明出处~ 首发于掘金~Eventloop 不可怕,可怕的是遇上 Promise
题外话:来 segmentfault 试水~ 啊哈哈哈啊哈哈