经典 Promise 面试代码题

180 阅读7分钟

对Promise不熟悉的可以先看一下下列文章,本文代码出处均来源下列文章

【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节 - 掘金 (juejin.cn)

字节飞书面试——请实现promise.all - 掘金 (juejin.cn)

1. 使用 Promise 实现每隔1秒输出1,2,3

原出处: 【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

const arr = [1, 2, 3];
arr.reduce((pre, value) => {
  return pre.then(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(value);
        resolve(value);
      }, 1000);
    });
  });
}, Promise.resolve());

2. 使用 Promise 实现红绿灯交替重复亮

原出处: 【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

// 亮灯函数
const light = function (timer, callback) {
  return new Promise((resolve) => {
    setTimeout(() => {
      callback();
      resolve();
    }, timer);
  });
};
const step = function () {
  Promise.resolve()
    .then(() => {
      return light(3000, red);
    })
    .then(() => {
      return light(2000, green);
    })
    .then(() => {
      return light(1000, yellow);
    })
    .then(() => {
      return step();
    });
};

step();

3. 实现 mergePromise 函数

原出处: 【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

const time = (timer) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timer);
  });
};
const ajax1 = () =>
  time(2000).then(() => {
    console.log(1);
    return 1;
  });
const ajax2 = () =>
  time(1000).then(() => {
    console.log(2);
    return 2;
  });
const ajax3 = () =>
  time(1000).then(() => {
    console.log(3);
    return 3;
  });

function mergePromise(arr) {
  const res = new Array(arr.length);

  let promise = Promise.resolve();

  arr.forEach((ajax, index) => {
    promise = promise.then(async () => {
      const value = await ajax();
      res[index] = value;
      return value;
    });
  });

  return promise.then(() => {
    return res;
  });
}

mergePromise([ajax1, ajax2, ajax3]).then((data) => {
  console.log("done");
  console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

4. 限制异步操作的并发个数并尽可能快的完成全部

原出处: 【建议星星】要就来45道Promise面试题一次爽到底(1.1w字用心整理) - 掘金 (juejin.cn)

思路:初始化一个长度为3的promises,用Promise.race(promises)获取返回最快的promise,按顺序取出未执行队列生成promise。将最早完成的promise代替,并对新生成的数组再次进行Promise.race(promises),重复上述步骤,为了便于理解,我们先写一段简易版代码

  // dataArr:需要执行的参数数组,这里先默认长度为8。handler:执行的函数
  function limitLoad(dataArr, handler) {
    let sequence = [].concat(dataArr); // 复制数据数组
    // 初始化一个长度为3的promises
    let promises = sequence.splice(0, 3).map((data, index) => {
      return handler(data).then(() => {
        // 完成后,返回下标
        return index;
      });
    });

    return Promise.race(promises)
      .then((fastestIndex) => {
        // 获取到已经完成的下标,将"容器"内已经完成的那一项替换
        promises[fastestIndex] = handler(dataArr[0]).then(() => {
          return fastestIndex; // 要继续将这个下标返回,以便下一次确定完成下标
        });
      })
      .then(() => {
        // 对新生成的promises进行新一轮race
        return Promise.race(promises);
      })
      .then((fastestIndex) => {
        // 按顺序取出未完成的对已完成的进行替换
        promises[fastestIndex] = handler(dataArr[1]).then(() => {
          return fastestIndex;
        });
      })
      .then(() => {
        // 对新生成的promises进行新一轮race
        return Promise.race(promises);
      })
      .then((fastestIndex) => {
        // 重复代码,只有取出数据位置依次递增
        promises[fastestIndex] = handler(dataArr[2]).then(() => {
          return fastestIndex;
        });
      })
      .then(() => {
        return Promise.race(promises);
      })
      .then((fastestIndex) => {
        promises[fastestIndex] = handler(dataArr[3]).then(() => {
          return fastestIndex;
        });
      })
      .then(() => {
        return Promise.race(promises);
      })
      .then((fastestIndex) => {
        promises[fastestIndex] = handler(dataArr[4]).then(() => {
          return fastestIndex;
        });
      })
      .then(() => {
        // 最后三个用.all来调用
        return Promise.all(promises);
      });
  }

理解上述代码后,我们对其进行优化

function limitConcurrence(promiseArr, limit) {
  const res = new Array(promiseArr.length);
  let sequence = [].concat(promiseArr); // 复制promise数组
  
  // 这一步是为了初始化 promises 这个"容器"
  let promises = sequence
    .splice(0, Math.min(limit, promiseArr.length))
    .map((promise, index) => {
      return promise().then((value) => {
        res[index] = value;
        // 返回下标是为了知道数组中是哪一项最先完成
        return index;
      });
    });
  // 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
  return sequence
    .reduce((pCollect, promise, pIndex) => {
      return pCollect
        .then(() => {
          return Promise.race(promises); // 返回已经完成的下标
        })
        .then((fastestIndex) => {
          // 获取到已经完成的下标
          // 将"容器"内已经完成的那一项替换
          promises[fastestIndex] = promise().then((value) => {
            res[pIndex + limit] = value;
            return fastestIndex; // 要继续将这个下标返回,以便下一次变量
          });
        })
        .catch((err) => {
          Promise.reject(err);
        });
    }, Promise.resolve()) // 初始化传入
    .then(() => {
      // 最后三个用.all来调用
      return Promise.all(promises).then(() => {
        return res;
      });
    });
}

5. 实现一个简单的 Promise

原出处: 从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节 - 掘金 (juejin.cn)

原出处实现更加完善,更多细节可查看原文 使用 queueMicrotask 创建微任务

思路

  • promise 是一个类
    • 字段
      • status 标识运行状态,运行状态一旦改变不可逆转
      • value 保存成功后的值
      • reason 保存失败的原因
      • onFulfilledCallback 保存成功后回调函数
      • onRejectedCallback 保存失败后回调函数
    • 函数
      • resolve 函数
        • 接受参数 val,将 val 赋值给 value 保存
        • 修改 status 状态为成功 fulfilled
        • 调用成功后的回调函数 onFulfilledCallback
      • reject 函数
        • 接受参数 res ,将 res 赋值给 reason 保存
        • 修改 status 状态为失败 rejected
        • 调用失败后的回调函数 onRejectedCallback
      • then 函数
        • 接受成功后的回调函数onFulfilledCallback ,失败后的回调函数onRejectedCallback
        • 若状态 status 未完成,则保存对应的回调函数
        • 若状态 status 已完成,根据状态执行对应的回调函数
class MyPromise {
  constructor(executor) {
    // executor 是一个执行器,进入会立即执行
    // 并传入resolve和reject方法
    executor(this.resolve, this.reject);
  }

  // 储存状态的变量,初始值是 pending
  status = PENDING;

  // 保存成功后的值
  value = null;

  // 保存失败的原因
  reason = null;
  // 存储成功回调函数
  onFulfilledCallback = null;
  // 存储失败回调函数
  onRejectedCallback = null;

  // 更改成功后的状态
  resolve = (val) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态修改为成功
      this.status = FULFILLED;
      // 保存成功之后的值
      this.value = val;
      // 判断成功回调是否存在,如果存在就调用
      this.onFulfilledCallback && this.onFulfilledCallback(val);
    }
  };

  // 更改失败后的状态
  reject = (res) => {
    // 只有状态是等待,才执行状态修改
    if (this.status === PENDING) {
      // 状态成功为失败
      this.status = REJECTED;
      // 保存失败后的原因
      this.reason = res;
      // 判断失败回调是否存在,如果存在就调用
      this.onRejectedCallback && this.onRejectedCallback(res);
    }
  };

  then(onFulfilled, onRejected) {
    // 判断状态
    if (this.status === FULFILLED) {
      // 调用成功回调,并且把值返回
      onFulfilled(this.value);
    } else if (this.status === REJECTED) {
      // 调用失败回调,并且把原因返回
      onRejected(this.reason);
    } else if (this.status === PENDING) {
      // 因为不知道后面状态的变化情况,所以将成功回调和失败回调存储起来
      // 等到执行成功失败函数的时候再传递
      this.onFulfilledCallback = onFulfilled;
      this.onRejectedCallback = onRejected;
    }
  }
}

6. 实现一个简单的 Promise.all

原出处: 字节飞书面试——请实现promise.all - 掘金 (juejin.cn)

Promise.MyAll = function (promises) {
  // 用于存储成功结果
  const res = new Array(promises.length);
  // 用于记录成功各数
  let count = 0;
  return new Promise((resolve, reject) => {
    // 依次执行各promise
    promises.forEach((item, index) => {
      Promise.resolve(item).then((val) => {
        // 成功后将结果存储到队列中
        res[index] = val;
        // 成功个数加一
        count += 1;
        // 当promises都成功时,返回成功的结果
        if (count === promises.length) {
          resolve(res);
        }
      }, reject);
    });
  });
};

7. 实现一个简单的 Promise.race

原出处: 字节飞书面试——请实现promise.all - 掘金 (juejin.cn)

// 和all类似,但是只需返回执行最快的结果即可
Promise.MyRace = function (promises) {
  return new Promise((resolve, reject) => {
    // 依次执行各promise
    promises.forEach((item) => {
      Promise.resolve(item).then((val) => {
        resolve(val);
      }, reject);
    });
  });
};

8. 实现一个简单的 Promise.any

原出处: 字节飞书面试——请实现promise.all - 掘金 (juejin.cn)

// 和all类似,但是只需返回执行最快的结果即可
Promise.MyAny = function (promises) {
  const reason = new Array(promises.length);
  let count = 0;
  return new Promise((resolve, reject) => {
    // 依次执行各promise
    promises.forEach((item, index) => {
      Promise.resolve(item).then(
        (val) => {
          // 只要成功一个,就将结果返回
          resolve(val);
        },
        (res) => {
          // 失败的原因保存
          reason[index] = res;
          // 失败个数加一
          count++;
          // 全部都失败时,返回失败队列
          if (count === promises.length) {
            reject(reason);
          }
        }
      );
    });
  });
};

9. 实现一个简单的 Promise.allSettled

字节飞书面试——请实现promise.all - 掘金 (juejin.cn)

// 全部完成时将结果返回,没有rejected状态
Promise.MyAllSettled = function (promises) {
  const res = new Array(promises.length);
  let count = 0;
  return new Promise((resolve) => {
    // 依次执行各promise
    promises.forEach((item, index) => {
      Promise.resolve(item).then(
        (val) => {
          // 成功的结果保存
          res[index] = { status: "fulfilled", value: val };
          // 完成个数加一
          count++;
          // 全部都完成时,返回结果
          if (count === promises.length) {
            resolve(res);
          }
        },
        (res) => {
          // 失败的原因保存
          res[index] = { status: "rejected", value: res };
          // 完成个数加一
          count++;
          // 全部都完成时,返回结果
          if (count === promises.length) {
            resolve(res);
          }
        }
      );
    });
  });
};

10. 实现一个动态并发池

type taskCallbacks = () => Promise<any>;
type ITask = { id: number; taskCallbacks: taskCallbacks };

/**动态并发池 */
export class PromisePoolDynamic<T> {
  /**最大并发数量 */
  private limit: number;
  /**当前正在跑的数量 */
  private runningCount: number;
  /**等待队列 */
  private queue: ITask[];

  /**动态并发池 - 构造函数
   * @param maxConcurrency 最大并发数量
   */
  constructor(maxConcurrency: number) {
    this.limit = maxConcurrency;
    this.runningCount = 0;
    this.queue = [];
  }

  taskOnWait(id) {
    return this.queue.find((item) => item.id === id);
  }

  /** 添加任务 **/
  addTask(args: ITask) {
    const { id, taskCallbacks } = args;
    if (this.taskOnWait(id)) {
      return;
    }
    if (this.runningCount < this.limit) {
      // 并发数量没满则运行
      this.runTask({ id, taskCallbacks });
    } else {
      // 并发数量满则加入等待队列
      this.queue.push({ id, taskCallbacks });
    }
  }
  /** 运行任务 **/
  private runTask(args: ITask) {
    const { id, taskCallbacks } = args;
    this.runningCount++; //当前并发数++
    taskCallbacks()
      .then((result) => {
        this.runningCount--;
        this.checkQueue();
      })
      .catch((error) => {
        this.queue = this.queue.filter((item) => item.id !== id);
        this.runningCount--;
        this.checkQueue();
      });
  }

  /** 运行完成后,检查队列,看看是否有在等待的,有就取出第一个来运行 **/
  private checkQueue() {
    if (this.queue.length > 0 && this.runningCount < this.limit) {
      const nextTask = this.queue.shift()!;
      this.runTask(nextTask);
    }
  }
}