汇总下最近看到的三个前端题目

348 阅读4分钟

    不知道大家对于最近一堆面经是怎么样的看法,我不喜欢看的是一堆相似题目比如作用域,闭包或者原型链的,这都已经烂大街了的东西,说再多也没什么特别多的干货。但是对一些新颖的题目还是很有好感的。

    最近看见了几个挺喜欢的题目来和大家分享一下,我觉得我写的答案没人家写的好,所以就不献丑了。第一个题是在我挺佩服的前端@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相反。

    总结语:

      感谢三位提供的学习材料,学习始终是自己的事情,也许现在懂得不会很多,但是一步步积累去探索终究还是会有收获的。