Eventloop不可怕,可怕的是遇上Promise

224 阅读12分钟
有关Eventloop+Promise的面试题大约分以下几个版本——得心应手版、游刃有余版、炉火纯青版、登峰造极版和究极变态版。假设小伙伴们战到最后一题,以后遇到此类问题,都是所向披靡。当然如果面试官们还能想出更变态的版本,算我输。
版本一:得心应手版
考点:eventloop中的执行顺序,宏任务微任务的区别。
吐槽:这个不懂,没得救了,回家重新学习吧。
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
setTimeout(()=>{
console.log(1)
},0)
Promise.resolve().then(()=>{
console.log(2)
})
console.log(3)

这个版本的面试官们就特别友善,仅仅考你一个概念理解,了解宏任务(marcotask)微任务(microtask),这题就是送分题。
笔者答案:这个是属于Eventloop的问题。main script运行结束后,会有微任务队列和宏任务队列。微任务先执行,之后是宏任务。
版本二:游刃有余版这一个版本,面试官们为了考验一下对于Promise的理解,会给题目加点料:
考点:Promise的executor以及then的执行方式
吐槽:这是个小坑,promise掌握的熟练的,这就是人生的小插曲。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
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
[JavaScript]
纯文本查看
复制代码
1
2
3
4
5
6
7
8
PromiseConstructor = function Promise(executor) {
//...
try {
executor(bind(internalResolve, this, state), bind(internalReject, this, state));
} catch (err) {
internalReject(this, state, err);
}
};

这里可以很清除地看到Promise中的executor是一个立即执行的函数。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
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)。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
//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中无其他异步操作。)。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
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也可以是地狱……
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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呢??
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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是否会影响结果??
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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给骗了,这题不难。
相信大家也看到过此类的题目,我这里有个相当简易的解释,不知大家是否有兴趣。
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的优先级详解
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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上述代码?下方先给出笔者的版本:
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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用于解决这个问题,简化了下源码,大概是下面这个样子:
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// 不太准确的一个描述
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
槽点:笔者都不知道那个可能先出现
[JavaScript]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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"之间的优先级,因平台而异。


文章转载至:https://juejin.cn/post/6844903808200343559