异步循环

准备面试的时候看到一道笔试题:

输出以下代码运行结果,为什么?如果希望每隔 1s 输出一个结果,应该如何改造?注意不可改动 square 方法

const list = [1, 2, 3];
const square = (num) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num * num);
        }, 1000);
    });
};

function test() {
    list.forEach(async (x) => {
        const res = await square(x);
        console.log(res);
    });
}
test();

forEach 的 polyfill 参考:MDN-Array.prototype.forEach()

Array.prototype.forEach = function (callback) {
    for (let index = 0; index < this.length; index++) {
        callback(this[index], index, this);
    }
};

运行结果: 结果比较容易看出来,forEach 函数内部是异步的,不能阻塞,所以题目中三个 timeout 几乎是同时加入到宏任务队列,同时输出 1,4,9

这里主要考察的是如何实现异步循环,最简单的改造方法是直接把 foreach 循环改成 for in 或者 for of 循环,简单直接。

function test() {
    for(let i = 0; i < list.length; i++) {
        let res = await square(list[i]);
        console.log(res);
    }
}

但是自己刚开始看到这道题思路没有想到这里,想用 Promise 的链式进行处理,通过 forEach 将我们需要按顺序执行的回调组装成一个操作链,这样的话 list 的遍历函数无论是用 foreach 还是 for in、for of 都是一样的结果,我们通过遍历 list 组装了一个异步回调链,配合 square 函数定时 1s 执行 resolve 就能实现间隔 1s 执行一步回调,输出一个结果,直接上代码:

function test() {
    var p = Promise.resolve();
    list.forEach((x) => {
        p = p.then(() => square(x).then((res) => console.log(res)));
    });
    // p = Promise.resolve().then(() => square(1).then((res) => console.log(res))).then(() => square(2).then((res) => console.log(res))).then(() => square(3).then((res) => console.log(res)))
}

下面这2种写法也可以

function test() {
    var p = Promise.resolve();
    list.reduce((pre, current) => {
        return pre.then(() => square(current).then(res => console.log(res)));
    }, p);
}
function test() {
    var p = Promise.resolve();
    list.reduce(async (pre, current) => {
        await pre;
        let result = await square(current);
        console.log(result);
        return result;
    }, p);
}

看到这道题目的时候自己虽然想到 Promise 的这个解决思路,但是这个链式的组装硬是调试了很久才完全搞定,对 Promise 的掌握还不够熟练。

关于这道题的更多讨论 可以看这里