前端优化,从少触发一次开始:防抖与节流

一、场景

在前端页面中,用户的点击等操作经常会触发服务端请求。正常点击一次,只会执行一次处理函数:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>高频点击示例</title>
</head>
<body>
    <button id="btn">提交</button>

    <script>
        const btn = document.getElementById('btn')
        btn.addEventListener('click', handle)

        function handle() {
            console.log('提交了')
        }
    </script>
</body>
</html>

但如果用户在短时间内连续点击,handle 就会连续执行,请求也会被频繁发送。单次请求没有问题,过高的请求频率却可能给服务端带来额外的性能压力。

因此,需要对高频行为进行控制。防抖和节流解决的是同一个问题,但控制执行的方式不同。

二、防抖

防抖是指:在规定时间内没有新的事件触发,才执行处理函数。如果等待期间再次触发事件,就取消上一次计时并重新开始。

在看防抖的实现之前,我们要先理解闭包。闭包可以简单理解为:一个函数能够记住并继续访问它定义时所在作用域中的变量。即使外层函数已经执行结束,只要内部函数仍然被外部引用,它所引用的外层变量就可以继续使用。

未做处理时,连续点击会让 handle 反复执行。怎样才能把多次点击合并,只在用户停止操作后执行一次?

防抖需要在多次点击之间保存上一次的计时器。利用闭包保存 timer,就能让连续点击共享同一个计时器:每次点击先清除旧计时器,再重新计时。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖基础实现</title>
</head>
<body>
    <button id="btn">提交</button>

    <script>
        const btn = document.getElementById('btn')
        btn.addEventListener('click', debounce(handle, 1000))

        function handle() {
            console.log('提交了')
        }

        function debounce(fn, delay) {
            let timer
            return function () {
                clearTimeout(timer)
                timer = setTimeout(() => {
                    fn()
                }, delay)
            }
        }
    </script>
</body>
</html>

在这段代码中,debounce 返回的事件处理函数引用了外层的 timer,两者形成闭包。事件监听器保存着这个返回函数,之后每次点击调用的都是它,访问的也是同一个 timer

只要点击还在继续,旧计时器就会被清除并重新创建;停止点击 1 秒后,最后一次计时完成,handle 才会执行。

优化解决handle中的 this 指向问题

上面的代码实现了基本防抖。不过,新的问题也出现了:当前版本直接使用 fn() 调用原函数,假如 handle 需要通过 this 操作当前按钮,该怎么办?

handle 直接作为事件处理函数时,this 指向触发点击的按钮;被包装后直接调用 fn(),这个指向就无法保留。要让 handle 继续使用当前按钮,可以通过 call 指定函数执行时的 this

html 复制代码
<script>
    const btn = document.getElementById('btn')
    btn.addEventListener('click', debounce(handle, 1000))

    function handle() {
       console.log('提交了', this)
    }

    function debounce(fn, delay) {
        let timer
        return function () {
            clearTimeout(timer)
            timer = setTimeout(() => {
                fn.call(this)
            }, delay)
        }
    }
    </script>

优化解决handle中可能存在的事件参数问题

浏览器触发点击事件时,会把返回的函数作为事件处理函数执行。定时器中的箭头函数沿用外层的 this,再由 fn.call(this) 传给 handle,因此 handle 仍然可以访问当前按钮。

现在 this 已经保留下来了。但还有一个问题:假如 handle 还需要读取点击事件对象 e,当前包装函数并没有接收和传递这个参数,又该怎么办?

让返回的事件处理函数接收 e,执行原函数时再把它传进去:

html 复制代码
<script>
    const btn = document.getElementById('btn')
    btn.addEventListener('click', debounce(handle, 1000))

    function handle(e) {
        console.log('提交了', this, e)
    }

    function debounce(fn, delay) {
        let timer
        return function (e) {
            clearTimeout(timer)
            timer = setTimeout(() => {
                fn.call(this, e)
            }, delay)
        }
    }
</script>

浏览器把事件对象交给防抖后的处理函数,fn.call(this, e) 再将当前按钮和事件对象一起交给 handle。至此,防抖函数终于大成了,现在既控制了执行频率,也保留了原事件处理函数需要的信息。

三、节流

防抖会一直等待操作停下来。如果希望事件持续触发时,处理函数仍能按照一定的时间间隔执行,就需要节流。

节流是指:在规定时间内只执行一次。事件可以持续触发,但只有距离上一次执行达到指定间隔,处理函数才会再次执行。

下面通过时间差判断是否放行本次点击:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>节流示例</title>
</head>
<body>
    <button id="btn">提交</button>

    <script>
        const btn = document.getElementById('btn')
        btn.addEventListener('click', throttle(handle, 1000))

        function handle(e) {
            console.log('提交了', this, e)
        }

        function throttle(fn, delay) {
            let preTime = Date.now()
            return function (e) {
                let curTime = Date.now()
                if (curTime - preTime >= delay) {
                    fn.call(this, e)
                    preTime = curTime
                }
            }
        }
    </script>
</body>
</html>

preTime 保存用于比较的时间基准,curTime 记录本次点击时间。当两次时间的差值达到 1 秒时,handle 才会执行,并把 preTime 更新为本次执行时间;未达到间隔的点击会被忽略。

相关推荐
默_笙1 小时前
🚤 CSS 布局的"圈地运动":BFC 就是浏览器的独立领地
前端·javascript
我爱写代码i1 小时前
AI对话绘画数字人源码 - uniapp前端
前端·人工智能·uni-app
OpenTiny社区2 小时前
太酷了!装上OpenTiny dsh‑genui这个插件,你的DeepSeek Harness点击就能干活了!
前端·ai编程
用户938515635072 小时前
大模型记忆指南:从短时缓存到永久记忆,手把手带你吃透 LangChain Memory 管理
javascript·人工智能
ModyQyW2 小时前
vite-plugin-uni-pages 更新了什么
前端·uni-app
xqchen2 小时前
Web Components 普及困境深度解析:技术标准与工程实践的落差
javascript
爱丶不疚2 小时前
写给前端工程师的现代 Python 工程化最佳实践:从 pnpm 到 uv,从 CommonJS 到 src-layout
javascript·python·typescript
前端大卫2 小时前
H5 渲染 PDF 并添加高亮的两种方案【附源码】
前端
因_崔斯汀3 小时前
用 AI 生成 Three.js 沉浸式网站,代码与 Skill 均已开源
前端·人工智能