前言

写这篇文章,是因为在最近一次字节的面试中,一道异步题,我写不出来,并且没什么思路。于是我痛定思痛,必须搞明白,才有了这篇文章。如果觉得有用,点赞支持一下,抚慰我我受伤的心灵 ~~

本文我将尽量使用 setTimeout、promise、async/await 这三种方式写出题解。

实现每隔 1 秒输出 1,2,3

不要看这道题简单,其实有很多可以考察。我们从简单开始理解再到复杂。

此题的变形有:实现每隔1秒请求接口,实现每隔几秒刷新时间,或者换个间隔时间等等,换汤不换药,为了更好适应变形写出其他题,这里用数组arr存数据。

//公共代码
const arr = [1, 2, 3];

setTimeout 实现

使用回调嵌套,注意记录 count

function timeout(count = 0) {
if (count === arr.length) return;
setTimeout(() => {
console.log(arr[count]);
timeout(++count);
}, 1000);
}
timeout();

promise 实现

通过不停的在 promise 后面叠加 .then ,实现间隔输出,这里也可以用 for,但是就相当于用 for 实现 reduce 了,所以这里直接用 reduce:

arr.reduce((p, x) => {
return p.then(() => {
return new Promise((r) => {
setTimeout(() => r(console.log(x)), 1000);
});
});
}, Promise.resolve());

async 实现

在 setTimeout 实现的基础上改,需要将函数包装为 async 函数,await 后跟的函数应该返回 promise,在 promise 里间隔一秒再 resolve ,这样就实现了。

async function timer() {
for (let i = 0; i < arr.length; i++) {
console.log(await _promise(arr[i]));
}
}
function _promise(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, 1000);
});
}
timer();

红绿灯交替重复亮

也是一道经典的面试题了,显然这里要递归调用,还需要确保顺序执行:我把这道题抽象一下:“有几个函数,我希望按顺序每间隔一定时间链式执行,如此循环”

//公共代码
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function yellow() {
console.log("yellow");
}

setTimeout 实现

显然面试官并不想看到你这样写,我也不打算写的~~ ,但是想起一位伟人说过的话:“只要能做出来,总比屁都憋不出的好”,于是我写了:

const light = function () {
setTimeout(() => {
red();
setTimeout(() => {
green();
setTimeout(() => {
yellow();
light();
}, 1000);
}, 2000);
}, 3000);
};
light();

效果是确实三秒 red 、两秒 green 和一秒 yellow ,诶,还能递归,我愿称之为“暴力”,当然它还有一个响亮的名字“回调地狱”。

好了,应该不用我多说回调地狱的坏处吧,来看 Promise 的实现:

promise 实现

const light = function (cb, time) {
return new Promise((resolve) => {
setTimeout(() => {
cb();
resolve();
}, time);
});
};
const step = function () {
Promise.resolve()
.then(() => {
return light(red, 3000);
})
.then(() => {
return light(green, 2000);
})
.then(() => {
return light(yellow, 1000);
})
.then(() => {
return step();
});
};
step();

async 实现

用 Promise 写会发现我们的代码长度很长,要是有十个灯就要写 10 个 then ,那有没有更简洁优雅的写法呢?当然、promise 能写出来,换成 async 来写更是轻轻松松咯:

function light(cb, timer) {
return new Promise(function (resolve, reject) {
setTimeout(() => {
cb();
resolve();
}, timer);
});
}
async function step() {
await light(red, 3000);
await light(green, 2000);
await light(yellow, 1000);
step();
}
step();

以下三个题目就不用 setTimeout 实现了,技术价值不大,或者有哪位友人有想法,可以评论留言给我~~

链式异步请求

有一个请求数组,要求按序执行并保存数据。类似于,先干一件事,这件事干完再干另一件事,我把这类题概括为链式请求。

//公共代码
function test(time, val) {
return new Promise((resolve) => {
setTimeout(resolve(), time);
}).then(() => {
console.log(val);
return val;
});
}
const ajax1 = () => test(1000, 1),
ajax2 = () => test(3000, 2),
ajax3 = () => test(2000, 3);

promise 实现

function mergePromise(ajaxArray) {
const data = [];
let promise = Promise.resolve();
ajaxArray.forEach((ajax) => {
promise = promise.then(ajax).then((res) => {
data.push(res);
return data;
});
});
return promise;
}

mergePromise([ajax1, ajax2, ajax3]).then((data) => {
console.log(data);
});

async 实现

async function mergePromise(ajaxArray) {
const data = [];
ajaxArray.forEach(async (ajax) => {
let res = await ajax();
data.push(res);
});
return Promise.resolve(data);
}

并发调度

这类题目有很多,核心考察就是 限制运行任务的数量。

为了能快速理解,我先讲一个通俗的例子:首先要限制数量,我们可以用一个栈,栈不能超过两格(假设限制数量为2),当放进去的两个任务,一个快一些先执行完,那么弹出该任务,接下一个,如此类推。。。

进阶:两个请求一直占着位置,没有请求回数据,因为它们没执行完成导致后面的请求也进不来,导致阻塞,怎么办呢。。。第一肯定是要判断阻塞,两个请求占的时间过久。第二记录这两个请求并清空栈,允许其他链接请求。最后根据场景,对数据进行处理,比如你需要对没请求的数据再重新请求,或者提示等。

为了展示更加直观,我选了最经典的一道面试题:

setTimeout 实现

用 setTimeout 实现需要 注意 的是它是直接 add 的 time 和 val ,而不是返回 promise 的函数,所以可以在 add 里实现:

//设计并发调度器, 最多允许两个任务运行
const scheduler = new Scheduler(2);
//这里的timer有的会写1有的会直接写1000,需要灵活解题
scheduler.addTask(5, "1");
scheduler.addTask(3, "2");
scheduler.addTask(1, "3");
scheduler.addTask(2, "4");
scheduler.start();
//输出:2314

思路:

  1. 用一个 count 记录并发的数量,用一个 taskList 数组保存任务。
  2. addTask 如名字,将任务一一存入 taskList 。
  3. 递归调用 start ,递归结束条件没有数据了,进入条件没有超过并发数。再通过 count 记录并发数量,从数组取出来一个 count++ ,执行完一个 count-- 。
class Scheduler {
constructor(maxNum) {
this.maxNum = maxNum;
this.count = 0;
this.taskList = [];
}
addTask(time, val) {
this.taskList.push([time, val]);
}
start() {
if (!this.taskList.length) return;
if (this.count < this.maxNum) {
var [time, val] = this.taskList.shift();
this.count++;
setTimeout(() => {
console.log(val);
this.count--;
this.start();
}, time * 1000);
this.start();
}
}
}

promise 实现

用 promise 写的话,实例代码就应该是下面这样:

const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});

const scheduler = new Scheduler();

const addTask = (time, order) => {
scheduler.add(() => timeout(time).then(() => console.log(order)));
};

addTask(5000, "1");
addTask(3000, "2");
addTask(1000, "3");
addTask(2000, "4");

需要注意的是使用 promise 实现的话也是离不开循环 .then 的,所以我们抽出一个函数来实现 then 的链式调用。

  1. 需要一个函数来实现 add 记录要执行的 promiseCreator ,还需要一个函数在执行的时候就去第一个就可以了。
  2. 要求只有一个 add 函数,所以我们需要在 add 里记录 promiseCreator 以及执行 run 。
  3. run 来触发异步函数的执行,这里的触发有两处,一处为 add 一个 promise 就 run ,另一个是自己执行完一个再 then 里执行 run ,当大于 max 时阻止继续 run 。

    这里如果想不明白的话,可以换一个生活里的场景。比如吃火锅,我喜欢吃虾滑,虾滑一个个下锅,煮熟就把它放到碗里,可碗就那么大只能放两个虾滑,吃一个才能从锅里取一个,直到锅里没有虾滑了。相信有了上述的这个场景,你能写出不一样的题解,这是我实现的既符合题意又相对简洁的 promise 实现:

class Scheduler {
constructor() {
this.taskList = [];
this.maxNum = 2;
this.count = 0;
}
add(promiseCreator) {
this.taskList.push(promiseCreator);
this.run();
}
run() {
if (this.count >= this.maxNum || this.taskList.length == 0) {
return;
}
this.count++;
this.taskList
.shift()()
.then(() => {
this.count--;
this.run();
});
}
}

async 实现

最简单地写法还得是 async (这里换了一种写法,你也可以用类实现),然后帮助理解如果没有 start 函数,怎么直接在 add 函数中实现逻辑:

  1. 用一个 count 记录并发的数量,用一个 taskList 数组保存任务。
  2. 异步函数 add 接受异步任务返回 promise 。
  3. 这里没有递归调用, add 一个异步任务,就执行,并用 count 记录并发数量。
  4. 关键思想:当并发数超过限制,我们 await 一个不被 resolve 的 promise ,当完成了一个请求有位置了,才 resolve。
function scheduler(maxNum) {
let taskList = [];
let count = 0;

return async function add(promiseCreator) {
if (count >= maxNum) {
await new Promise((resolve, reject) => {
taskList.push(resolve);
});
}
count++;
const res = await promiseCreator();
count--;
if (taskList.length > 0) {
taskList.shift()();
}
return res;
};
}

待优化

依旧是根据场景来的,如果并发的两个任务,一直没被处理,那么会一直等待导致后面的请求也发不了。为了防止这种阻塞,可以怎样优化呢?如果你有好的想法,非常欢迎在评论区留言,❤ღ( ´・ᴗ・` )。

promise 手写

这一部分建议看 PromiseA+ 实现,掘金有相关文章,我自己也看过也实践过完整一遍的 promise 手写,这里建议自己手写一遍,如果想了解的更细节循序渐进的写的话,可以搜 B 站的高赞 Promise 实现视频(防止有打广告嫌疑就不放链接了)跟着写。

这里我写一个最基础的 Promise 实现手写:

function Promise() {
self.status = "pending"; // 默认的状态,只能改变一次
self.value = null; // 成功的值
self.reason = null; // 失败的原因
self.onFulfilledCb = []; // 存放then成功的回调
self.onRejectedCb = []; // 存放then失败的回调

function resolve(value) {
// 成功
if (self.status === "pending") {
self.status = "fulfilled";
self.value = value;
self.onFulfilledCb.forEach(function (fn) {
fn();
});
}
}

function reject(reason) {
// 失败
if (self.status === "pending") {
self.status = "rejected";
self.reason = reason;
self.onRejectedCb.forEach(function (fn) {
fn();
});
}
}

try {
executor(resolve, reject);
} catch (e) {
// 抛出错误,走失败的方法
reject(e);
}
}

Promise.prototype.then = function (onFulfilled, onRejected) {
const self = this;
if (self.status === "fulfilled") {
onFulfilled(self.value);
}
if (self.status === "rejected") {
onRejected(self.reason);
}
if (self.status === "pending") {
// TODO 这里需要注意只能实现Promise实例可以多次.then没有实现then的链式调用(需要返回一个新的Promise)
// 将成功的回调添加到数组中
self.onFulfilledCb.push(function () {
onFulfilled(self.value);
});
self.onRejectedCb.push(function () {
onRejected(self.reason);
});
}
};

Promise.all

Promise.all 实现思路:

  • 批量执行 Promise,返回一个 promise 实例;
  • 全部成功才算成功,返回全部执行结果;
  • 有一个失败就算失败,返回第一个失败结果;
const all = function (promises) {
return new Promise((resolve, reject) => {
let result = [];
let times = 0;

// 将成功结果放入数组中对应的位置
const processSuccess = (index, val) => {
// result.push(val); //使用push异步会出现混乱的情况
result[index] = val;
if (++times === promises.length) {
resolve(result); // 全部执行成功,返回 result
}
};

// 遍历处理集合中的每一个 promise
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === "function") {
// 调用这个p的 then 方法
p.then((data) => {
// 按照执行顺序存放执行结果
processSuccess(i, data);
}, reject);
} else {
// 普通值,直接按照执行顺序放入数组对应位置
processSuccess(i, p);
}
}
});
};

你可以简化这个代码,但是注意测试不要写错了:测试代码:

// 测试
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("val = 1; timeout = 1000");
resolve(1);
}, 1000);
}),
new Promise((resolve, reject) => {
setTimeout(() => {
// console.log("reason = 2; timeout = 3000")
// reject()
console.log("val = 2; timeout = 3000");
resolve(2);
}, 3000);
}),
new Promise((resolve, reject) => {
setTimeout(() => {
console.log("val = 3; timeout = 2000");
resolve(3);
}, 2000);
}),
4,
])
.then((data) => {
console.log("then", data);
})
.catch((err) => {
console.log("catch", err);
});

Promise.race

Promise.race 实现思路:谁快就返回谁

const race = function (promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === "function") {
p.then(resolve, reject); // 使用第一个执行成功的结果
} else {
resolve(p);
}
}
});
};

下面这两个虽然我没有看到过在那个面试出现过,但是还是还是列出来,你可以看成前面知识都理解后的加餐,不难的。

Promise.allsettled

  • 存在失败结果也会拿到全部执行结果,不会走 catch;
  • 解决了 Promise.all 不能拿到失败执行结果的问题; Promise.allsettled 以及 Promise.any 测试代码
const p1 = Promise.resolve(1);
const p2 = new Promise((resolve, reject) => setTimeout(resolve(2), 2000));
const p3 = new Promise((resolve, reject) => setTimeout(reject(3), 1000));

Promise.allSettled([p1, p2, p3, 4]).then((results) =>
console.log("res", results)
/**输出:
* res [{ status: 'fulfilled', value: 1 },
* { status: 'fulfilled', value: 2 },
* { status: 'rejected', reason: 3 },
* { status: 'fulfilled', value: 4 }]
* */

自实现:

function allSettled(promises) {
const result = new Array(promises.length);
let times = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === "function") {
p.then((data) => {
result[i] = { status: "fulfilled", value: data };
times++;
if (times === promises.length) {
resolve(result);
}
}).catch((err) => {
result[i] = { status: "rejected", reason: err };
times++;
if (times === promises.length) {
resolve(result);
}
});
} else {
result[i] = { status: "fulfilled", value: p };
times++;
if (times === promises.length) {
resolve(result);
}
}
}
});
}

Promise.any

  • 返回第一个成功结果,全部失败才返回失败;
  • 解决了 Promise.race 只能拿到第一个执行完成(不管成功/失败)的结果;
function any(promises) {
const rejectedArr = [];
let rejectedTimes = 0;
return new Promise((resolve, reject) => {
if (promises == null || promises.length == 0) {
reject("无效的 any");
}
for (let i = 0; i < promises.length; i++) {
let p = promises[i];
if (p && typeof p.then === "function") {
p.then(
(data) => {
resolve(data); // 使用最先成功的结果
},
(err) => {
// 如果失败了,保存错误信息;全失败,any 才失败
rejectedArr[i] = err;
rejectedTimes++;
if (rejectedTimes === promises.length) {
reject(rejectedArr);
}
}
);
} else {
resolve(p);
}
}
});
}

//上个测试用例:
/**输出:
* res 4
* */

总结

经过这几个面试题的 promise 和 async 的分别实现,会发现使用 promise 虽然整个流程线性化,单还是会包含大量的 then。ES7 引入的async/await,可以说是 “回调地狱” 的终极解决方案,代码逻辑也更加清晰。

这篇文章总结了很久,也搜集了很多资料,真的希望你能一次学懂,而不是像我之前一样一知半解。

最后因为篇幅有限,这里只是针对面试题给出了我的解题思路和方案。而如果你还有其他的异步场景的面试题而我没有给出的,可以评论出来我们一起解决。或者有其他的建议或问题也可以评论出来,友好交流。

🌸 非常感谢你看到这,如果觉得不错的话点个赞 👍 或收藏 ⭐ 吧 ~~

今天也是在努力变强不变秃的 HearLing 呀 💪 🌸