资讯专栏INFORMATION COLUMN

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

olle / 2774人阅读

摘要:就是每次传入的函数最后是的任务之后,开始执行,可以看到此时会批量执行中的函数,而且还给这些中回调函数放入了一个这个很显眼的函数之中,表示这些回调函数是在微任务中执行的。下一模块会对此微任务中的插队行为进行详解。

有关Eventloop+Promise的面试题大约分以下几个版本——得心应手版、游刃有余版、炉火纯青版、登峰造极版和究极{{BANNED}}版。假设小伙伴们战到最后一题,以后遇到此类问题,都是所向披靡。当然如果面试官们还能想出更{{BANNED}}的版本,算我输。

版本一:得心应手版
考点:eventloop中的执行顺序,宏任务微任务的区别。

吐槽:这个不懂,没得救了,回家重新学习吧。

setTimeout(()=>{
   console.log(1) 
},0)
Promise.resolve().then(()=>{
   console.log(2) 
})
console.log(3) 

这个版本的面试官们就特别友善,仅仅考你一个概念理解,了解宏任务(marcotask)微任务(microtask),这题就是送分题。

笔者答案:这个是属于Eventloop的问题。main script运行结束后,会有微任务队列和宏任务队列。微任务先执行,之后是宏任务。

PS:概念问题

有时候会有版本是宏任务>微任务>宏任务,在这里笔者需要讲清楚一个概念,以免混淆。这里有个main script的概念,就是一开始执行的代码(代码总要有开始执行的时候对吧,不然宏任务和微任务的队列哪里来的),这里被定义为了宏任务(笔者喜欢将main script的概念多带带拎出来,不和两个任务队列混在一起),然后根据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: [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")
})

笑容逐渐{{BANNED}},同样这个我们可以自己心中排一个队列:

第一轮

current task: promise1,promise3

micro task queue: [promise2的第一个thenpromise3的第一个then]

第二轮

current task: then11,promise2,then31

micro task queue: [promise2的第一个thenpromise1的第二个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,所以现在最新版本的应该是“零成本”的一个异步。
版本五:究极{{BANNED}}版

饕餮大餐,什么{{BANNED}}的内容都往里面加,想想就很丰盛。能考到这份上,只能说面试官人狠话也多。

考点: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 startasync1 startasync2promise1script 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年模拟的既视感,奈何面试官们都太凶残了,为了“折磨”面试者无所不用其极,怎么{{BANNED}}怎么来。不过因此笔者算是彻底掌握了Eventloop的用法,因祸得福吧~

有小伙伴看到最后嘛?来和笔者聊聊你遇到过的的Eventloop+Promise的{{BANNED}}题目。

欢迎转载~但请注明出处~首发于掘金~Eventloop不可怕,可怕的是遇上Promise

题外话:来segmentfault试水~啊哈哈哈啊哈哈

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103054.html

相关文章

  • 【前端面试分享】- 寒冬求职上篇

    摘要:记录下我遇到的面试题,都有大佬分享过,附上各个大佬的文章,总结出其中的主要思想即可。推荐黑金团队的文章前端缓存最佳实践推荐名扬的文章浅解强缓存和协商缓存状态码重点是等,要给面试官介绍清楚。前言 在这互联网的寒冬腊月时期,虽说过了金三银四,但依旧在招人不断。更偏向于招聘高级开发工程师。本人在这期间求职,去了几家创业,小厂,大厂厮杀了一番,也得到了自己满意的offer。 整理一下自己还记得的面试...

    shinezejian 评论0 收藏0
  • js学习之异步处理

    摘要:学习开发,无论是前端开发还是都避免不了要接触异步编程这个问题就和其它大多数以多线程同步为主的编程语言不同的主要设计是单线程异步模型。由于异步编程可以实现非阻塞的调用效果,引入异步编程自然就是顺理成章的事情了。 学习js开发,无论是前端开发还是node.js,都避免不了要接触异步编程这个问题,就和其它大多数以多线程同步为主的编程语言不同,js的主要设计是单线程异步模型。正因为js天生的与...

    VioletJack 评论0 收藏0
  • 精读《async/await 是把双刃剑》

    摘要:本周精读内容是逃离地狱。精读仔细思考为什么会被滥用,笔者认为是它的功能比较反直觉导致的。同时,笔者认为,也不要过渡利用新特性修复新特性带来的问题,这样反而导致代码可读性下降。 本周精读内容是 《逃离 async/await 地狱》。 1 引言 终于,async/await 也被吐槽了。Aditya Agarwal 认为 async/await 语法让我们陷入了新的麻烦之中。 其实,笔者...

    2shou 评论0 收藏0
  • 为什么找到工作的是你,技术差的还是你?

    摘要:当然是否需要培训这个话题,得基于两个方面,如果你是计算机专业毕业的,大学基础课程学的还可以,我建议不需要去培训,既然有一定的基础,那就把去培训浪费的四个月,用去实习,培训是花钱,实习是挣钱,即使工资低点,一正一负自己算算吧。 上周一篇《程序员平时该如何学习来提高自己的技术》火了之后,「非著名程序员」微信公众号的后台经常收到程序员和一些初学者的消息,问一些技术提高的问题,而且又恰逢毕业季...

    yeyan1996 评论0 收藏0

发表评论

0条评论

olle

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<