彙總下最近看到的三個前端題目

黑布發表於2019-03-23

    不知道大家對於最近一堆面經是怎麼樣的看法,我不喜歡看的是一堆相似題目比如作用域,閉包或者原型鏈的,這都已經爛大街了的東西,說再多也沒什麼特別多的乾貨。但是對一些新穎的題目還是很有好感的。

    最近看見了幾個挺喜歡的題目來和大家分享一下,我覺得我寫的答案沒人家寫的好,所以就不獻醜了。第一個題是在我挺佩服的前端@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相反。

    總結語:

      感謝三位提供的學習材料,學習始終是自己的事情,也許現在懂得不會很多,但是一步步積累去探索終究還是會有收穫的。


相關文章