【前端基础】2 - 9 防抖和节流

§2-9 防抖和节流

2-9.1 防抖

防抖(debounce)指的是在单位时间内,频繁触发事件,只执行最后一次触发的事件。

防抖是一项常用的性能优化技术。若一个处理一个事件比较耗时或消耗性能(如数据处理、DOM 操作等),在短时间内大量反复触发该事件会导致性能下降,利用防抖技术则可以解决这一问题。

防抖技术常用在搜索框搜索推荐、手机号或邮箱输入检测等场景。

要求:在页面中有一个盒子,要求鼠标每在盒子上移动一次,盒子上的移动次数计数器自增且显示在盒子上。

<div class="box">
    <!-- 显示计数的标签 -->
    <h3></h3>
</div>
.box {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 300px;
    height: 300px;
    margin: 20px auto;
    background-color: #aaa;
    border-radius: 10px;
}

.box h3 {
    color: white;
    font-size: 80px;
    font-weight: 350;
}
// 获取元素
const box = document.querySelector('.box');
const cnt = box.querySelector('h3');

// 事件处理回调
let i = 0;		// 计数器
function mouseMove() {
    i++;
    cnt.innerHTML = `${i}`;
}

2-9.1.1 使用定时器实现防抖

实现防抖的本质上是,在触发事件时,启动计时器。计时器超时时执行真正的事件处理函数。但在此期间,若再次触发事件,清除原有的计时器,设置新的计时器,重新计时。这样,即便在短时间内重复触发大量的事件,也能保证只有最后一次被触发的事件能够被处理。

// 防抖函数
function debounce(fn, time) {
    let timer;  // 定时器
    // 返回事件处理的回调函数
    return function() {
        // 清除原定时器
        if (timer) clearTimeout(timer);
        // 新增定时器
        timer = setTimeout(fn, time);
    }
}

这里我们使用 debounce 函数封装事件处理的回调函数,并将其作为参数 fn 传入。那么,为元素添加事件处理器时,我们应当将 debounce 作为事件监听器的回调函数。但是,我们需要向 debounce 传入参数。

// 事件监听
box.addEventListener('mousemove', debounce(mouseMove, 500));

这样,就完成了事件防抖。

注意到 debounce 函数返回了一个函数。这是因为我们在为 addEventListener 传入回调函数时实际上调用了 debounce 函数而不是传入 debounce 函数,这会导致 debounce 函数立即执行,并将函数的返回值作为事件处理的回调函数。若 debounce 不返回函数而直接作为回调函数处理事件,则会导致在页面加载完成并执行到 addEventListener 方法时执行事件处理函数的逻辑。实际上这样做会导致 addEventListener 所接收到的回调函数为 null。因此,我们必须让防抖函数返回事件处理回调函数。

2-9.1.2 使用 Lodash.js 的 API 实现防抖

Lodash.js 中的 _.debounce 函数为我们封装好了防抖的实现。只需要如下调用即可:

box.addEventListener('mousemove', _.debounce(mouseMove, 500));

实际上,Lodash.js 中的防抖也是使用了定时器的方式实现。

2-9.2 节流

节流(throttle)指的是在单位时间内,频繁触发的事件,只执行一次。

节流的实现和防抖十分相似。但不同的是,节流会保证已经触发的事件先被执行,而不会被重置计时器,直到事件被处理后,才会开始处理新的事件。即节流会在计时器未超时期间,无论触发多少次事件,只执行一次事件处理。

节流作为另一种性能优化技术,常用于高频事件处理中,如页面滚动、尺寸缩放等。

我们仍然使用上述的鼠标滑动示例演示节流。

2-9.2.1 使用定时器实现节流

节流的实现与防抖十分类似,都需要使用定时器完成。

// 节流函数
function throttle(fn, time) {
    let timer = null;  // 定时器
    // 返回事件处理的回调函数
    return function() {
        if (!timer) {
            timer = setTimeout(() => {
                fn();           // 执行回调
                timer = null;   // 清空计时器
            }, time);
        }
    }
}

这里,节流需要判断是否已有等待处理的事件。若有,则跳过本次执行。若无,则执行本次事件处理。此外,要注意的是不能够在定时器内部使用 clearTimeout,这样做无法清除定时器,因为此时定时器还在创建之中(运行之中),无法被清除。

// 事件监听
box.addEventListener('mousemove', throttle(mouseMove, 500));

2-9.2.2 使用 Lodash.js 的 API 实现节流

Lodash.js 中的 _.throttle 函数为我们封装好了节流的实现。只需要如下调用即可:

box.addEventListener('mousemove', _.throttle(mouseMouse, 500));

实际上,Lodash.js 中的防抖也是使用了定时器的方式实现。

2-9.2.3 视频播放的节流案例

现在绝大多数的视频平台都支持记录播放位置的功能,本质上就是在页面加载完成(或视频第一帧加载完毕但后续帧的数据尚未加载)时读取用户的播放进度,然后将视频的当前时间跳转至上次观看的时间。

视频进度跳转可用事件 onloadeddata 结合事件监听器,从本地存储中读取用户播放进度并跳转即可。这是利用到了 <video> 标签中当前时间进度 currentTime 的可读写特性。

那么,在跳转前,我们需要记录用户当前的视频观看进度。视频在播放时,它的时间进度会不断发生改变,触发事件 ontimeupdate,我们可以利用该事件绑定事件监听器实时将用户观看进度写入本地存储中。但这会导致一个问题,该事件被高频出发从而影响性能。我们可以使用节流实现。

// 记录播放进度
const vid = document.querySelector('video');

// 每隔一秒记录一次播放进度
vid.addEventListener('ontimeupdate', ._throttle(() => {
    localStorage.setItem('currentTime', vid.currentTime);
}, 1000));
// 恢复播放进度
vid.addEventListener('onloadeddata', () => {
    vid.currentTime = localStorage.getItem('currentTime') || 0;
});

2-9.3 总结

性能优化方案 说明 使用场景
防抖 单位时间内,频繁触发事件,只执行最后一次 搜索框输入、手机号或邮箱格式验证等
节流 单位时间内,高频触发事件,只执行一次 鼠标移动、页面尺寸缩放、页面滚动等
posted @ 2024-03-26 22:53  Zebt  阅读(36)  评论(0)    收藏  举报