防抖(debounce)与节流(throttle)

331 阅读3分钟

防抖和节流是前端开发中经常使用的一种优化手段,它们都被用来控制一段时间内方法执行的次数,可以为我们节省大量不必要的开销

防抖(debounce)

当我们需要及时获知窗口大小变化时,我们会给window绑定一个resize函数,像下面这样:


window.addEventListener('resize', () => {
    console.log('resize')
});


我们会发现,即使是极小的缩放操作,也会打印数十次resize,也就是说,如果我们需要在onresize函数中搞一些小动作,也会重复执行几十次。但实际上,我们只关心鼠标松开,窗口停止变化的那一次resize,这时候,就可以使用debounce优化这个过程:

const handleResize = debounce(() => {
    console.log('resize');
}, 500);
window.addEventListener('resize', handleResize);

行上面的代码(你得有现成的debounce函数),在停止缩放操作500ms后,默认用户无继续操作了,才会打印resize

这就是防抖的功效,它把一组连续的调用变为了一个,最大程度地优化了效率

再举一个防抖的常见场景:

搜索栏常常会根据我们的输入,向后端请求,获取搜索候选项,显示在搜索栏下方。如果我们不使用防抖,在输入“debounce”时前端会依次向后端请求"d"、"de"、"deb"..."debounce"的搜索候选项,在用户输入很快的情况下,这些请求是无意义的,可以使用防抖优化

观察上面这两个例子,我们发现,防抖非常适于只关心结果,不关心过程如何的情况,它能很好地将大量连续事件转为单个我们需要的事件

为了更好理解,下面提供了最简单的debounce实现:返回一个function,第一次执行这个function会启动一个定时器,下一次执行会清除上一次的定时器并重起一个定时器,直到这个function不再被调用,定时器成功跑完,执行回调函数


const debounce = function(func, wait) {
    let timer;
    return function() {
        !!timer && clearTimeout(timer);
        timer = setTimeout(func, wait);
    };
};

那如果我们不仅关心结果,同时也关心过程呢?

节流(throttle)

节流让指定函数在规定的时间里执行次数不会超过一次,也就是说,在连续高频执行中,动作会被定期执行。节流的主要目的是将原本操作的频率降低

实例:

我们模拟一个可无限滚动的feed流

<div id="wrapper">
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
    <div class="feed"></div>
</div>
#wrapper {
    height: 500px;
    overflow: auto;
}
.feed {
    height: 200px;
    background: #ededed;
    margin: 20px;
}
const wrapper = document.getElementById("wrapper");
const loadContent = () => {
    const {
        scrollHeight,
        clientHeight,
        scrollTop
    } = wrapper;
    const heightFromBottom = scrollHeight - scrollTop - clientHeight;
    if (heightFromBottom < 200) {
        const wrapperCopy = wrapper.cloneNode(true);
        const children = [].slice.call(wrapperCopy.children);
        children.forEach(item => {
            wrapper.appendChild(item);
        })
    }
}
const handleScroll = throttle(loadContent, 200);
wrapper.addEventListener("scroll", handleScroll);

可以看到,在这个例子中,我们需要不停地获取滚动条距离底部的高度,以判断是否需要增加新的内容。我们知道,srcoll同样也是种会高频触发的事件,我们需要减少它有效触发的次数。如果使用的是防抖,那么得等我们停止滚动之后一段时间才会加载新的内容,没有那种无限滚动的流畅感。这时候,我们就可以使用节流,将事件有效触发的频率降低的同时给用户流畅的浏览体验。在这个例子中,我们指定throttle的wait值为200ms,也就是说,如果你一直在滚动页面,loadCotent函数也只会每200ms执行一次

同样,这里有throttle最简单的实现,当然,这种实现很粗糙,有不少缺陷(比如没有考虑最后一次执行),只供初步理解使用:

const throttle = function (func, wait) {
    let lastTime;
    return function () {
        const curTime = Date.now();
        if (!lastTime || curTime - lastTime >= wait) {
            lastTime = curTime;
            return func();
        }
    }
}