美文网首页vue、javascript
一道面试题让你更加了解事件队列

一道面试题让你更加了解事件队列

作者: 源大侠 | 来源:发表于2021-04-01 08:48 被阅读0次

    该题定义了一个同步函数对传入的数组进行遍历乘二操作,同时每执行一次就会给 executeCount 累加。最终我们需要实现一个batcher 函数,使用其对该同步函数包装后,实现每次调用依旧返回预期的二倍结果,同时还需要保证 executeCount 执行次数为1。

    let executeCount = 0
    const fn = nums => {
      executeCount++
      return nums.map(x => x * 2)
    }
    
    const batcher = f => {
      // todo 实现 batcher 函数
    }
    
    const batchedFn = batcher(fn);
    
    const main = async () => {
      const [r1, r2, r3] = await Promise.all([
        batchedFn([1,2,3]),
        batchedFn([4,5]),
        batchedFn([7,8,9])
      ]);
      
      //满足以下 test case
      assert(r1).tobe([2, 4, 6])
      assert(r2).tobe([8, 10])
      assert(r3).tobe([14, 16, 18])
      assert(executeCount).tobe(1)
    }
    

    抖机灵解法

    拿到题目的第一时间,我就想到了抖机灵的方法。直接面向用例编程,执行完之后重置下 executeCount就好了

    const batcher = f => {
      return nums => {
        try { 
          return f(nums) 
        } finally { 
          executeCount = 1 
        }
      }
    }
    

    当然除非你不在乎这次面试,否则一般不建议你用这种抖机灵的方法回答面试官(不要问我为什么知道)。由于executeCount 的值和fn() 函数的调用次数呈正相关,所以这道理也就换成了我们需要实现batcher()方法返回新的包装函数,该函数会被调用多次,但最终只会执行一次fn() 函数。

    setTimeout 解法

    由于题干中使用了 Promise.all(),我们自然而然想到使用异步去解决。也就是每次调用的时候会把所以的传参存下来,直到最后的时候再执行 fn() 返回对应的结果。问题在于什么时候触发开始执行呢?自然而然我们想到了类似 debounce 的方式使用setTimeout 增加延迟时间。

    const batcher = f => {
      let nums = [];
      const p = new Promise(res => 
        setTimeout(_ => res(f(nums)), 100)
      );
    
      return arr => {
        let s = nums.length;
        nums = nums.concat(arr);
        let e = nums.length;
        return p.then(r => r.slice(s, e));
      };
    };
    

    这里的难点在于预先定义了一个 Promise 在 100ms 之后才会resolve。返回的函数本质只是将参数推入到 nums 数组中,待 100ms 后触发 resolve 返回统一执行 fn()后的结果并获取对应于当前调用的结果片段。

    后来有群友反馈,实际上不用定义 100ms 直接 0ms 也是可以的。由于setTimeout 是在 UI 渲染结束之后才会执行的宏任务,所以理论上来说 setTimeout() 的最小间隔值无法设置为 0。它的最小值和浏览器的刷新频率有关系,根据MDN[1] 描述,它的最小值一般为 4ms。所以理论上它设置 0ms 和 100ms 效果是差不多的,都类似于 debounce 的效果。

    Promise 解法

    那么如何能实现延迟 0ms 执行呢?我们知道除了宏任务之外 JS 还有微任务,微任务队列是在 JS 主线程执行完成之后立即执行的事件队列。Promise 的回调就会存储在微任务队列中。于是我们将 setTimeout 修改成了 Promise.resolve(),最终发现也是可以实现同样的效果。

    const batcher = f => {
      let nums = [];
      const p = Promise.resolve().then(
        _ => f(nums)
      );
    
      return arr => {
        let s = nums.length;
        nums = nums.concat(arr);
        let e = nums.length;
        return p.then(r => r.slice(s, e));
      };
    };
    

    由于Promise 的微任务队列效果将_ => f(nums)推入微任务队列,待主线程的三次 batcherFn()调用都执行完成之后才会执行。之后p 的状态变为 fulfilled 后继续完成最终 slice 的操作。

    后记

    最终分析下来,其实这道理的本质就是要通过某些方法将fn() 函数的执行后置到主线程执行完毕,至于是使用宏任务还是微任务队列,就看具体的需求了。除了 setTimeout() 之外,还有 setInterval(), requestAnimationFrame() 都是宏任务队列。而微任务队列里除了有 Promise之外,还有 MutationObserver

    参考资料

    [1] MDN: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#reasons_for_delays

    相关文章

      网友评论

        本文标题:一道面试题让你更加了解事件队列

        本文链接:https://www.haomeiwen.com/subject/twwzhltx.html