前言
写这篇文章,是因为在最近一次字节的面试中,一道异步题,我写不出来,并且没什么思路。于是我痛定思痛,必须搞明白,才有了这篇文章。如果觉得有用,点赞支持一下,抚慰我我受伤的心灵 ~~
本文我将尽量使用 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
思路:
- 用一个
count记录并发的数量,用一个taskList数组保存任务。 addTask如名字,将任务一一存入taskList。- 递归调用
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 的链式调用。
- 需要一个函数来实现
add记录要执行的promiseCreator,还需要一个函数在执行的时候就去第一个就可以了。 - 要求只有一个
add函数,所以我们需要在add里记录promiseCreator以及执行run。 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 函数中实现逻辑:
- 用一个
count记录并发的数量,用一个taskList数组保存任务。 - 异步函数
add接受异步任务返回promise。 - 这里没有递归调用,
add一个异步任务,就执行,并用count记录并发数量。 - 关键思想:当并发数超过限制,我们
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 呀 💪 🌸