Promise面试题思考延伸

最近想起之前在V2EX上看到的一个问题:一个 async function 数组, 怎样一个一个顺序执行?

想做到的是,每过一秒,分别打印: this is 0 > this is 1 > this is 2 ~ this is 8

下面的代码结果是过一秒后全部执行。

是不是哪里写的不对呢,多谢指教

var jobs = [];

for (let i = 0; i < 8; i++) {
  jobs.push(async function () {
    setTimeout(function () {
      console.log("this is " + i);
    }, 1000);
  });
}

(async function () {
  for (const job of jobs) {
    await job();
  }
})();

这题考察对 Promise 、async/await 的理解。而这题又能让人联想到一个类似的setTimeout 循环问题——破解前端面试(80% 应聘者不及格系列):从 闭包说起

请问以下代码打印出什么数据

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(new Date(), i);
  }, 1000);
}

console.log(new Date(), i);

如何修改成每隔1秒打印一个数,以0、1、2、3、4、5 的顺序排列,并要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?

此题面试官想考察循环、闭包、闭包的解决(IIFE)、ES6 知识点(let、Promise)、ES7 的Async/await,以及setTimeout 的第三个参数等等知识点,具体看文章能明白一二

这里我们讲第一道题目,如果要实现题主所说的效果,缺少了什么?

已知:let形成块级作用域,意味着每次循环,jobs就 push 一个 async 函数,这些都是同步执行

但是注意,async 中的函数也是同步执行,只有等到 await 时才会进入微任务中,所以当

  • i=0时,jobs 塞入一个 setTimeout(function() { console.log(“this is 0” )})
  • i=1时,jobs 塞入一个 setTimeout(function() { console.log(“this is 1” )})
  • i=2时,jobs 塞入一个 setTimeout(function() { console.log(“this is 2” )})
  • i=3时,jobs 塞入一个 setTimeout(function() { console.log(“this is 3” )})
  • i=7时,jobs 塞入一个 setTimeout(function() { console.log(“this is 7” )})

继续往下执行

(async function () {
  for (const job of jobs) {
    await job();
  }
})();

这里题主了解到 await 需要 async 配合使用,就写了立即执行匿名函数,执行数组 jobs,但问题是 jobs 中的每个子项都是执行 async function(){setTimeout},这里的 async 有意义吗?

jobs.push(async function () {
  setTimeout(function () {
    console.log("this is " + i);
  }, 1000);
});

如果要让 await 暂停进程并恢复进程(即await job()),我们需要的是什么?

去掉 async,使其变成一个普通的函数,结果执行结果一致

jobs.push(function () {
  setTimeout(function () {
    console.log("this is " + i);
  }, 1000);
});

同样,将普通函数改成箭头函数也是如此,一秒之后打印还是0~7

根据网友总结:

  1. 对于 promise 对象,await 会阻塞函数执行,等待 promise 的 resolve 返回值,作为 await 的结果,然后再执行下一个表达式
  2. 对于非 promise 对象,比如 箭头函数、同步表达式等等,await 等待函数或者直接量的返回,而不是等待其执行结果

所以如果要让 await 每隔1秒执行一个 job,那就需返回一个 promise 实例,基于此逻辑进行改造

...
jobs.push(function () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve()
        	console.log("this is " + i)
    	}, 1000)
    })
});
...

这样,就解决了这个问题

我们的逻辑是在循环中,每次向 jobs 中塞入一个函数,这个函数返回的是一个实例 Promise(即 await 遇到后会暂停等异步结束再继续后续代码)。当执行 await job() 时,我们知道是循环 jobs,await 让其等待执行,执行完第一个后,再执行第二个,循序执行,每一个等待1秒钟,就达到题目的要求

这里我们了解到 await 等待的是一个 promise 实例(如果非 promise 实例,就不用等待),既然说到 Promise,我们就延伸一下,then 的链式调用

Promise 的 then 方法支持链式调用,它有哪几种情况?

  • 不 return(返回)值, 值延续上一个 resolved
  • return
    • return 非 Promise 实例
    • return Promise 实例

不 return

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    // 不返回任何值
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 undefined

return 非 Promise 实例

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    return "帝王johan";
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 帝王johan

return Promise 实例

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`${data} next`);
      }, 4000);
    });
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 johan next

以上三个例子可以得知,在 then 方法中的 onfulfilled 函数和 onrejected 函数,不仅支持不返回,而且支持非 Promise 实例的普通值,而且支持一个 Promise 实例。并且返回的这个 Promise 实例或非 Promise 实例的普通值将会传给下一个 then 方法的 onfulfilled 函数或者 onrejected 函数中

因为我们知道它是 Generator 函数的语法糖async 函数返回的是一个 Promise 对象,当函数执行时,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

我们来一道题来测试一下

const myPromise = (val) => Promise.resolve(val);
const delay = (duration) => {
  /**/
};
myPromise(`hello`)
  .then(delay(1000))
  .then((val) => console.log(val)); // hello

myPromise 是个 Promise 实例,传入值 hello,经过 Promise.resolve 传到then 中,然后经 delay 再传递给下一个 then,打印出val,所以 delay(1000) 会返回一个 Promise 实例,这样,第二个 then 才能打印出 hello

const delay = (duration) => (val) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(val);
    }, duration);
  });