有道题,得细说(一道异步相关的面试题)

2,519 阅读5分钟

念念不忘,必有回响(性格严肃的人跳过这一段,皮百万的不用)

最近,在一次正式场合下,遇到了一道检验js相关原理的题目,当时虎躯一震,这不是送分儿咩?不由分说,大笔一挥,写完之后还骄傲的叉了会儿腰,大概是这样事儿的:

(膨胀使我头大)

完事儿之后,似乎略有不妥,但是作为快乐风男,前进的道路上绝不回头,纵使身后洪水滔天。 然而缘分就是这么巧,一个非正式场合下,再次相遇,怎么能放过人前显圣的机会(读书人装逼不叫装逼,叫人前显圣)?一顿键盘后,准备再叉会儿腰,然而正确答案让我猝不及防。。。

看看这道小可爱

async function async1() {
    console.log(1)
    const result = await async2();
    console.log(3)
}

async function async2() {
    console.log(2);
}

Promise.resolve().then(() => {
    console.log(4)
})

setTimeout(() => {
    console.log(5)
})

async1();
console.log(6);

请写出打印结果。 对于相关原理不太了解的同学,想来是要翻车的;有点了解的同学,指定会是一副激动的心、颤抖的手,插着腰,敲出162345。 然而正确答案:

结果是意外的,这波儿腰就先别插了,咱们象征性的分析分析,这是啥原因。

分析

根据这道题的呈现,可以看出,涉及到的基本原理有以下几个方面:

  1. promise原理
  2. async-await原理
  3. 同步异步
  4. 宏任务微任务原理

每一个方面其实都包含不少的知识,在这里就不一一细讲了,毕竟网上到处都是(不是不想讲,也不是不会讲,只是之前有同学评论我,都9102年了,再写这种基础活该挨骂。我这向来都是从善如流,虚心听取)。所以咱们只讲涉及到的。

首先,同步异步就很常见了,一笔带过,同步,从上到下,从左到右,按顺序执行code;异步,code执行到该行为时,先收集起来,暂不执行,等到执行时机到来,在执行队列里收集到的行为。

然后,宏任务微任务,简单来说,均属异步行为,一般情况下,一个宏任务里面总是先顺序执行同步代码,再顺序执行该宏任务中的微任务(嵌套的话,会更复杂一些),等到都执行完毕,再进入下一个宏任务。啥是宏任务?script标签包含的code、setTimeout、setInterval、setImmediately、I/O等。啥是微任务?promise.then、process.nextTick等。

接着,promise,一个处理异步行为的工具,属于微任务,例题中相关代码为:

Promise.resolve().then(() => {
    console.log(4)
});

怎么理解这段代码? Promise.resolve()返回了一个promise对象(也叫thenable对象),并且这个对象立马被resolve。 但是由于resolve函数里面的code是一个异步的行为,所以尽管resolve是在then之前执行,但是,里面的异步行为是排在then执行之后才触发。(异步行为=》执行当前promise实例中存放then方法收集到的函数队列,这个队列是一个微任务队列), 然后这个对象的then方法收集了一个回调函数,放在promise实例的微任务回调队列里(then只是收集,并没有执行,是resolve的执行,才触发了微任务异步队列的执行),then会返回一个新的promise实例,但是这个这里不涉及,暂且不表。

最后,async-await,这道题里涉及到两个很关键的概念:

2. await 只能在 async 函数中使用。 await 后面可以跟普通的函数,也可以跟带有then方法的对象,也就是thenable。如果后面跟的是thenable时,await会收集thenable对象的原型对象上的then方法,并给其注入resolve和reject;然后阻塞当前作用域代码的执行,等待注入的resolve开启微任务异步队列的执行。如果后面不是thenable对象的话,直接开启微任务异步队列的执行。(此处感谢@茹挺进大佬的审查和建议) 执行这段代码,理解上述说明:

var o = {};
o.__proto__.then = function(resolve,reject){
        resolve(1);
    };

(async ()=>{
    var r = await o;
    console.log(r);
})();

注意:thenable对象中被注入的resolve函数,如果不执行,那么await将一直阻塞,当前作用域里,await后面的代码永远不会执行。

用分析结果执行代码

  1. 声明了async1,
  2. 声明了async2,
  3. Promise.resolve()返回了一个promise对象,并且这个对象立马被resolve 然后这个对象的then方法收集了一个回调函数,放在promise实例的微任务回调队列里。 所以此时,当前宏任务队列里的微任务队列里,只有一个promise的队列,里面有一个打印4的回调。
  4. 遇到了setTimeout,回调直接被置入下一个宏任务队列。
  5. 执行async1,打印1, 然后执行async2,打印2, 但是此时遇到了await,await做了两件事,1.返回了async1的函数,2。阻塞了async2中await后面的函数,先开启当前微任务异步队列的执行。
  6. await返回后,执行后面的同步代码,打印6,此时同步的代码执行完毕。
  7. 同步的代码执行完毕后,执行刚才开启的微任务异步队列,打印4,此时await开启的微任务异步队列执行完毕。
  8. await开启的微任务异步队列执行完毕后,解除阻塞,打印3。
  9. 当前宏任务打印完毕,执行下一个宏任务,打印5.

对await的怀疑

await会如我们分析的这样去做么?它会和promise的微任务队列这样配合? 我们直接写一个例子试一下:

async function async1() {
    console.log(1)
    const result = await async2();
    console.log(3)
}

async function async2() {
    console.log(2);
    return {
        then:(res)=>{
            console.log(7);
            res();
        }
    }
}

Promise.resolve().then(() => {
    console.log(4)
})

setTimeout(() => {
    console.log(5)
})

async1();
console.log(6)

根据我们上面讲的原理,结果应该是1,2,6,4,7,3,5。 你去打印试试吧,在谷歌里哦,防止翻车~

写在最后

需要声明的一点是,我不是一个教授者,我只是一个分享者、一个讨论者、一个学习者,有不同的意见或新的想法,提出来,我们一起研究。分享的同时,并不只是被分享者在学习进步,分享者亦是。

知识遍地,拾到了就是你的。

既然有用,不妨点赞,让更多的人了解、学习并提升。