不知道大家對於最近一堆面經是怎麼樣的看法,我不喜歡看的是一堆相似題目比如作用域,閉包或者原型鏈的,這都已經爛大街了的東西,說再多也沒什麼特別多的乾貨。但是對一些新穎的題目還是很有好感的。
最近看見了幾個挺喜歡的題目來和大家分享一下,我覺得我寫的答案沒人家寫的好,所以就不獻醜了。第一個題是在我挺佩服的前端@serialcoder的部落格中看見的,話不多說,看題:
@serialcoder給出的答案是:
const limitConcurrency = (fn, max) => {
const pendingTasks = new Set();
return async function(...args) {
while (pendingTasks.size >= max) {
await Promise.race(pendingTasks);
}
const promise = fn.apply(this, args);
const res = promise.catch(() => {});
pendingTasks.add(res);
await res;
pendingTasks.delete(res);
return promise;
};
};
async function sendRequest(urls, max, callback) {
const limitFetch = limitConcurrency(fetch, max);
await Promise.all(urls.map(limitFetch));
callback();
}複製程式碼
作者將最大併發數的限制放在了limitConcurrency中的async 函式中,將所有請求結束的回撥函式執行放在了sendRequest函式中,挺喜歡作者用函式柯里化的思想,加上用map函式自動的將urls中每一項的值傳進limitFetch中,最後用Promise.all來確定請求集體結束。
在看這段程式碼的時候,我起初在納悶為什麼要賦值這一段。
const res = promise.catch(() => {});複製程式碼
看到了Promise.all醒悟到了:萬一fetch請求中發生錯誤,Promise.all就直接結束了,那就意味著回撥函式會比預想的早觸發。
還有一位是幻☆精靈寫的思想和我很相近,但是實現起來比我簡潔乾脆。都是利用迴圈加上條件,直接用fetch函式來進行請求,因為fetch是源自promise,也就是說迴圈的主執行緒會比promise的微任務先執行,執行完了max數目的迴圈,才開始進行fetch請求。但是用了finally這個方法讓人眼前一亮,然後在返回的結果中相對應的進行操作,這段程式碼就比較通俗易懂:
function sendRequest(urls, max, callback) {
const len = urls.length;
let idx = 0;
let counter = 0;
function _request() {
while (idx < len && max > 0) {
fetch(urls[idx++]).finally(() => {
max++;
counter++;
if (counter === len) {
return callback();
} else {
_request();
}
});
}
}
_request();
}複製程式碼
第二個題目是來自尹光耀面經中的題:
function machine() {
}
machine('ygy').execute()
// start ygy
machine('ygy').do('eat').execute();
// start ygy
// ygy eat
machine('ygy').wait(5).do('eat').execute();
// start ygy
// wait 5s(這裡等待了5s)
// ygy eat
machine('ygy').waitFirst(5).do('eat').execute();
// wait 5s
// start ygy
// ygy eat複製程式碼
這題目粗略想想大概能知道是用陣列將一個個任務塞進去,然後任務從前往後執行,類似於佇列。但是不同之處在於wait和waitFirst兩個區別,wait是按照正常的push進去,所以他執行在start ygy之前,waitFirst塞入時間比start ygy晚,但卻執行於start yay之前,這就想到了unshift方法。等待時間可以用setTimeout來控制,一個接著一個執行是不是很容易想到promise的鏈式用法,再擴充套件一步就是加以運用 async和await來進行。
下面給出原作者的答案:
function machine(name) {
return new Action(name)
}
const defer = (time, callback) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(callback())
}, time * 1000)
})
}
class QueueItem {
constructor(defer, callback) {
this.defer = defer;
this.callback = callback;
}
}
class Action {
queue = []
constructor(name) {
this.name = name;
this.queue.push(new QueueItem(0, () => console.log(`start ${this.name}`)))
}
do(eat) {
this.queue.push(new QueueItem(0, () => console.log(`${this.name} ${eat}`)))
return this;
}
wait(time) {
this.queue.push(new QueueItem(time, () => console.log(`wait ${time}s`)))
return this;
}
waitFirst(time) {
this.queue.unshift(new QueueItem(time, () => console.log(`wait ${time}s`)))
return this;
}
async execute() {
while(this.queue.length > 0) {
const curItem = this.queue.shift();
if (!curItem.defer) {
curItem.callback();
continue;
}
await defer(curItem.defer, curItem.callback)
}
}
}
複製程式碼
在評論中發現serialcoder也給出了不同的答案,思想是差不多的,就是又包了一層非同步,他這asyncPipe函式和接下講的題目解法很類似於是就拿出來先看看了:
const defer = sec => new Promise(resolve => setTimeout(resolve, sec * 1000));
const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);
function Machine(name) {
const tasks = [];
let needToWaitFirst = false;
function _do(str) {
const task = () => {
console.log(`${name} ${str}`);
};
tasks.push(task);
return this;
}
function wait(sec) {
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.push(task);
return this;
}
function waitFirst(sec) {
needToWaitFirst = true;
const task = async () => {
console.log(`wait ${sec}s`);
await defer(sec);
};
tasks.unshift(task);
return this;
}
function execute() {
const task = () => {
console.log(`start ${name}`);
};
if (needToWaitFirst) {
tasks.splice(1, 0, task);
} else {
tasks.unshift(task);
}
asyncPipe(...tasks)();
}
return {
do: _do,
wait,
waitFirst,
execute
};
}複製程式碼
第三個題目來自蒙面大蝦的面經,實現一個compose。話不多話直接看答案,答案參考更詳細的解析:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}複製程式碼
很簡單明瞭的答案,將函式陣列進行遍歷,當reduce函式沒傳入預設引數a時,a會被認為是陣列的第一項,b則是陣列第二項。舉例來說,
funcs = [fn1, fn2, fn3];複製程式碼
假設傳入的...args為[1,2],那在第一次reduce的時候,a為fn1,b為fn2。因為此時函式並沒有給與a預設值,所以第一次reduce相當於 返回 (...args) => fn1( fn2(...args))。
第二次reduce的時候,這時候的a就為 (...args) => fn1( fn2(...args)),它是一個函式。而b就是fn3,此時的a函式中的...args === fn3([1,2])。
類似的pipe就很簡單可以得出了,執行順序是從fn1 -> fn2 -> fn3,與compose相反。
總結語:
感謝三位提供的學習材料,學習始終是自己的事情,也許現在懂得不會很多,但是一步步積累去探索終究還是會有收穫的。