JS 手写系列:防抖和节流

90 阅读4分钟

前言

在开发场景中经常会遇到一些这样的事件:

  1. scorll、focus
  2. mousedown、mousemove
  3. keydown、keyup

这些频繁触发的事件,如果包含回调函数则不仅可能导致性能降低还可能直接导致页面崩溃;如果包含 ajax 请求则会大量占用服务器带宽节流(throttle)和防抖(debounce)是两个类似又有些不同,同时在开发场景中性能优化最常用的优化方案。

我们举个示例代码,实现事件频繁的触发并逐步优化以实现节流(throttle)和防抖(debounce)的效果:

// index.js
<!DOCTYPE html>
<html>
<body>
    <div id = "container" style = "width: 100%;height:200px; line-height: 200px; text-align:center; color: #fff; background-color: #444;font-size: 30px;">
    </div>
</body>
<script>
    var count = 1;
    var container = document.getElementById('container');

    function getUserAction() {
        container.innerHTML = count++;
        console.log(count)
    };
    
    function debounce(){
    }
    
    function throttle(){
    }
    
    container.onmousemove = getUserAction;
</script>
</html>

代码效果如下:随着鼠标的移动函数会不断的执行,在一秒内执行了200次余 debounce1.png 如果函数是复杂的回调函数或是 ajax 请求,浏览器将完全反应不过来。假设 1 秒触发了 50 次,每个回调就必须在 1000 / 50 = 20ms 内完成,否则就会有卡顿出现。

为了解决这个问题,一般有两种解决方案:

  1. debounce 防抖
  2. throttle 节流

防抖(debounce)

原理

言简意赅:函数的执行者冷静下来后(不一直抖动后),才真正执行。

防抖,会存在一个 wait time 概念。wait time 为 n 秒,只有最后一次触发事件后 n 秒,才真正执行一次事件。换句话说,在频繁触发事件时,如果在一个事件触发的 n 秒内又触发了这个事件,那就以新事件触发的时间为准,向后 n 秒后,才执行。比如频繁触发的某一函数,防抖可以只在最后一次触发后执行。

在了解了防抖的原理之后,我们要写出防抖函数是很容易的

<script>
...
    function debounce(func, wait){
        var timer;
        return function(){
            clearTimeout(timer);
            timer = setTimeout(func, wait)
        }
    }
    
    container.onmousemove = debounce(getUserAction, 1000)
</script> 

在使用了 debounce 处理后,在鼠标停止移动的1秒后才会执行一次操作。

我们现在实现了最基本的防抖函数,但是我们在实际使用者会发现存在一些问题:

  1. this 指向
  2. event 指向

1. this 指向

分别在使用 debounce 前和使用 debounce 后打印this。

var count = 1;
var container = document.getElementById('container');

function getUserAction(e) {
    console.log(this)
    container.innerHTML = count++;
};

在不使用 debounce 前,我们如果在 getUserAction 打印 this,指向的是 id 为 container 的元素。

在使用 debounce 后,this应该指向绑定的DOM元素,可this 却不正确的指向了window

因此需要将 this 正确指向,我们修改下代码:

// 第二版
function debounce(func, wait) {
    var timer;
    return function () {
        var _this = this;
        clearTimeout(timer)
        timer = setTimeout(function(){
            func.apply(_this) // 修复this指向问题
        }, wait);
    }
}

经过我们第二版,使用了闭包,去获取上文的this,现在 this 已经可以正确指向了。

2. event 对象

function getUserAction(e) {
    console.log(e)
    container.innerHTML = count++;
};

在不使用 debounce 前,我们如果在 getUserAction 打印 e,监听的是 mouseevent 这个事件。

在使用 debounce 后,e 却不再正确监听,打印为 undefined

所以我们需要监听正确的事件,我们修改下代码:

// 第三版
function debounce(func, wait) {
    var timer;
    return function () {
        var _this = this;
        var _args = arguments;
        clearTimeout(timer)
        timer = setTimeout(function(){
            func.apply(_this, _args) // 修复 this、arguments 指向问题
        }, wait);
    }
}

这样我们就用JS实现了防抖,优化了操作时的频繁触发。

节流(throttle)

原理

在一定时间内,持续触发时,事件只会在设定的时间内触发一次。

使用时间戳

当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

// 时间戳
function throttle(func, wait) {
    var context, args;
    var old = 0;

    return function() {
        var now = +new Date();
        _this = this;
        _args = arguments;
        if (now - old > wait) {
            func.apply(_this, _args);
            old = now;
        }
    }
}

使用定时器

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

// 定时器
function throttle(func, wait) {
    var context, args;
    var timer;
    return function() {
        _this = this;
        _args = arguments;
        if (!timer) {
            timer = setTimeout(function(){
                timeout = null;
                func.apply(_this, _args)
            }, wait)
        }
    }
}

推荐阅读

JS实现节流和防抖之防抖(一)