前言

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

本文我将尽量使用 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 写会发现我们的代码长度很长,要是有十个灯就要写 10then ,那有没有更简洁优雅的写法呢?当然、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 实现需要 注意 的是它是直接 addtimeval ,而不是返回 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 一个 promiserun ,另一个是自己执行完一个再 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 一个不被 resolvepromise ,当完成了一个请求有位置了,才 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
* */

总结

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

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

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

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

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