彻底搞懂了异步场景面试题,多种题解
|字数总计:3.7k|阅读时长:15分钟|阅读量:
前言
写这篇文章,是因为在最近一次字节的面试中,一道异步题,我写不出来,并且没什么思路。于是我痛定思痛,必须搞明白,才有了这篇文章。如果觉得有用,点赞支持一下,抚慰我我受伤的心灵 ~~
本文我将尽量使用 setTimeout、promise、async/await
这三种方式写出题解。
实现每隔 1 秒输出 1,2,3
不要看这道题简单,其实有很多可以考察。我们从简单开始理解再到复杂。
此题的变形有:实现每隔1
秒请求接口,实现每隔几秒刷新时间,或者换个间隔时间等等,换汤不换药,为了更好适应变形写出其他题,这里用数组arr
存数据。
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);
scheduler.addTask(5, "1"); scheduler.addTask(3, "2"); scheduler.addTask(1, "3"); scheduler.addTask(2, "4"); scheduler.start();
|
思路:
- 用一个
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 = []; self.onRejectedCb = [];
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") { 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[index] = val; if (++times === promises.length) { resolve(result); } };
for (let i = 0; i < promises.length; i++) { let p = promises[i]; if (p && typeof p.then === "function") { 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("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)
|
自实现:
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) => { rejectedArr[i] = err; rejectedTimes++; if (rejectedTimes === promises.length) { reject(rejectedArr); } } ); } else { resolve(p); } } }); }
|
总结
经过这几个面试题的 promise
和 async
的分别实现,会发现使用 promise
虽然整个流程线性化,单还是会包含大量的 then
。ES7 引入的async/await
,可以说是 “回调地狱” 的终极解决方案,代码逻辑也更加清晰。
这篇文章总结了很久,也搜集了很多资料,真的希望你能一次学懂,而不是像我之前一样一知半解。
最后因为篇幅有限,这里只是针对面试题给出了我的解题思路和方案。而如果你还有其他的异步场景的面试题而我没有给出的,可以评论出来我们一起解决。或者有其他的建议或问题也可以评论出来,友好交流。
🌸 非常感谢你看到这,如果觉得不错的话点个赞 👍 或收藏 ⭐ 吧 ~~
今天也是在努力变强不变秃的 HearLing 呀 💪 🌸