1
写在前面:
本人没有这道题的标准答案,百度上也查不到相关的题解,以下思路都是本人个人想法,如有更好的答案,欢迎提出!

网易前端校招的一道面试题,对一个事件循环内所有调用进行防抖。

image.png

大家对防抖应该都有所耳闻,但什么是对一个事件循环内的调用进行防抖?本人也从未听说过,看第一遍的时候也没有想法。

但是这道题总结下来,应该是要实现两个需求:

  1. 多次同步调用 G ,只执行第一次
  2. 分别使用 setTimeout 异步调用 G 和同步调用 G ,各执行一次
既然是两个需求,那就一个一个来

先来实现第一个需求,这个比较简单,甚至也不用关心事件循环。跟 once 函数的实现比较类似,每次调用之前先判断一下是否已经调用,如已调用就不再调用了。

function debounce(func) {
    let called = false;
    return function() {
        if(!called) {
            called = true;
            func();
        }
    }
}

虽然感觉哪里不对,但是第一个需求已经实现了。各位同学肯定也发现了,上面的代码存在一个问题,也就是同步调用 G 之后,没法通过 setTimeout 再次调用了,因为 called 已经变成了 true

接下来实现第二个需求。为了让 setTimeout 可以再次调用,我们需要通过一种手段,在所有同步代码执行完毕之后,将 called 再次改为 false

既然要在同步代码执行完毕之后修改 called ,我们能不能用 setTimeout 呢?按照这个思路,代码如下:

function debounce(func) {
    let called = false;
    return function() {
        if(!called) {
            called = true;
            func();
            setTimeout(() => {
                called = false;
            }, 0)
        }
    }
}

测试了一下发现,第一个需求没有问题,但是第二个需求还是只打印了一次。这是因为调用顺序的问题,在第二个需求下,setTimeout(G, 0) 先进入任务队列,然后执行到下一行 G() 的时候,防抖函数中的 setTimeout 才进入任务队列。大家都知道队列是先进先出,那么 setTimeout(G, 0) 会先于防抖函数中的 setTimeout 执行,当 setTimeout(G, 0) 执行的时候, called 还是 true ,所以还是只打印一次。

要解决这个问题,就要找到一种机制,它的执行时机在同步代码之后,在 setTimeout 等异步任务执行之前。我们知道,JS 中的异步任务其实分为两种,宏任务微任务

JS 在执行宏任务之前都会检查微任务队列,如果队列不为空,就先执行微任务,直到清空微任务队列,再执行宏任务

我们知道,setTimeoutsetInterval 、回调函数、文件I/O 等都属于宏任务,因此,为了实现题目中的需求,我们可以使用微任务修改 called 。常见的微任务包括 PromiseMutationOserverprocess.nextTick ,我们使用最常见的 Promise 来实现。

function debounce(func) {
    let called = false;
    return function() {
        if(!called) {
            called = true;
            func();
            Promise.resolve().then(() => {
                called = false;
            })
        }
    }
}

经过测试,完美实现题目中的两个需求。


一杯绿茶
199 声望17 粉丝

人在一起就是过节,心在一起就是团圆