【前端三剑客-43/Lesson92(2025-12-30)】闭包在性能优化中的应用——防抖节流

4 阅读8分钟

🚀 性能优化主题

在前端开发中,性能优化是一个永恒的话题。当用户与页面交互时,某些事件可能会被频繁触发,导致大量的计算或网络请求,从而影响页面性能和用户体验。闭包作为JavaScript中一个强大的特性,在性能优化方面有着广泛的应用。

📋 需要优化的场景

🎯 防抖

在处理用户输入时,经常会遇到"帕金森"式的问题:

  • 执行太密集:用户快速输入时,每个字符都会触发事件
  • 任务比较复杂:每次触发都需要执行耗时的操作,比如网络请求

💡 典型应用场景

  1. 搜索建议功能

    • 百度搜索的ajax suggest
    • 代码编辑器的智能提示
    • 如果请求太快,服务器开销就大
    • 如果请求太慢,用户体验就差
  2. 窗口resize事件

    • 调整浏览器窗口大小时会频繁触发
    • 可能需要重新计算布局或发送统计数据
  3. 滚动事件

    • 用户滚动页面时持续触发
    • 可能需要加载更多内容或更新UI状态

⏱️ 防抖的原理与实现

🎯 核心概念

防抖的核心思想是:管你keyup触发多少次,我只执行最后一次

当事件被频繁触发时,防抖函数会确保在指定的时间内,只执行一次真正的处理函数。如果在等待时间内事件再次被触发,则重新计时。

🔧 实现原理

防抖的实现主要借助两个关键技术:

  1. 定时器:使用setTimeout推迟执行
  2. 闭包:将定时器ID保存到闭包中,方便清除

💻 代码实现

function debounce(fn, delay) {
    var id  // 自由变量,不会被销毁
    
    return function(args) {
        if (id) clearTimeout(id)
        
        var that = this
        id = setTimeout(function() {
            fn.call(that, args)
        }, delay)
    }
}

📝 代码详解

  1. 高阶函数debounce是一个高阶函数,因为它接收函数作为参数,并返回一个新的函数

  2. 闭包的作用

    • id变量定义在debounce函数内部
    • 返回的内部函数可以访问id变量
    • 即使debounce函数执行完毕,id变量也不会被垃圾回收
    • 这就是闭包的神奇之处
  3. 执行流程

    • 每次触发事件时,先清除之前的定时器(如果存在)
    • 保存当前的this上下文(因为定时器回调会丢失this
    • 设置新的定时器,在延迟后执行真正的函数
    • 如果在延迟期间再次触发,会清除之前的定时器,重新计时
  4. 为什么需要保存this

    • 定时器回调函数中的this指向全局对象(非严格模式)
    • 使用fn.call(that, args)可以确保正确的this指向

🎢 节流的原理与实现

🎯 核心概念

节流的核心思想是:每隔一定时间执行一次

与防抖不同,节流会确保函数以固定的频率执行,不管事件触发得多么频繁。

🔧 实现原理

节流的实现需要记录上一次执行的时间,并根据当前时间判断是否应该执行:

  1. 时间戳:记录上次执行的时间
  2. 延迟定时器:处理边界情况

💻 代码实现

function throttle(fn, delay) {
    let last
    let deferTimer
    
    return function() {
        let that = this  // 保存this,防止丢失
        let _args = arguments  // 保存参数
        let now = +new Date()  // 获取当前时间戳
        
        // 如果上次执行过,且还没到执行时间
        if (last && now < last + delay) {
            clearTimeout(deferTimer)
            deferTimer = setTimeout(function() {
                last = now
                fn.apply(that, _args)
            }, delay)
        } else {
            last = now
            fn.apply(that, _args)
        }
    }
}

📝 代码详解

  1. 变量说明

    • last:记录上次执行的时间戳
    • deferTimer:延迟定时器ID
    • now:当前时间戳(使用+new Date()快速转换)
  2. 执行逻辑

    • 如果是第一次执行(last为空),直接执行
    • 如果距离上次执行时间不足delay,设置延迟定时器
    • 如果距离上次执行时间超过delay,立即执行
  3. 为什么需要deferTimer

    • 处理最后一次触发后,可能需要延迟执行的情况
    • 确保最后一次操作也能被执行
  4. 使用apply而非call

    • arguments是类数组对象,不能直接展开
    • apply可以接收类数组对象作为参数

🔄 防抖与节流的区别

📊 核心区别对比

特性防抖节流
执行时机只执行最后一次每隔固定时间执行一次
实现方式setTimeout时间戳 + setTimeout
适用场景搜索建议、表单验证滚动事件、动画帧
执行频率不确定(取决于最后一次触发)固定频率

🎮 形象比喻

防抖就像电梯门:

  • 有人按按钮时,门会延迟关闭
  • 如果在延迟期间又有人按,重新计时
  • 只有当没有人按按钮时,门才会关闭

节流就像FPS游戏的射速:

  • 就算一直按着鼠标射击
  • 也只会在规定射速内射出子弹
  • 射击频率是固定的

📝 总结

  • 函数的防抖和节流都是防止某一时间频繁触发
  • 这两兄弟原理不一样,应用场景也不同
  • 防抖是某段时间内只执行一次
  • 函数节流是间隔时间执行

🌟 实际应用示例

🔍 搜索建议 - 防抖

const input = document.getElementById('search')

function searchSuggest(keyword) {
    console.log('ajax request', keyword)
    // 发送ajax请求获取搜索建议
}

let debounceSearch = debounce(searchSuggest, 500)

input.addEventListener('keyup', function(e) {
    debounceSearch(e.target.value)
})

为什么用防抖

  • 用户输入时,每个字符都会触发keyup事件
  • 如果不防抖,每个字符都会发送一个请求
  • 使用防抖后,只有用户停止输入500ms后才发送请求
  • 节约了服务器资源,提升了用户体验

📜 滚动事件 - 节流

window.addEventListener('scroll', throttle(function() {
    console.log('检查是否滚动到底部')
    // 检查是否滚动到底部,加载更多内容
}, 200))

为什么用节流

  • 滚动事件触发频率极高(每秒可能触发几十次)
  • 如果不节流,会执行大量重复的计算
  • 使用节流后,每200ms执行一次
  • 既能及时响应,又不会过度消耗性能

🔧 完整示例代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖与节流</title>
</head>
<body>
    <div>
        <h3>不使用防抖/节流</h3>
        <input type="text" id="undebounce" placeholder="输入试试看控制台" />
        
        <h3>使用防抖</h3>
        <input type="text" id="debounce" placeholder="输入试试看控制台" />
        
        <h3>使用节流</h3>
        <input type="text" id="throttle" placeholder="输入试试看控制台" />
    </div>
    
    <script>
        function ajax(content) {
            console.log('ajax request', content)
        }
        
        function debounce(fn, delay) {
            var id
            return function(args) {
                if (id) clearTimeout(id)
                var that = this
                id = setTimeout(function() {
                    fn.call(that, args)
                }, delay)
            }
        }
        
        function throttle(fn, delay) {
            let last
            let deferTimer
            return function() {
                let that = this
                let _args = arguments
                let now = +new Date()
                
                if (last && now < last + delay) {
                    clearTimeout(deferTimer)
                    deferTimer = setTimeout(function() {
                        last = now
                        fn.apply(that, _args)
                    }, delay)
                } else {
                    last = now
                    fn.apply(that, _args)
                }
            }
        }
        
        const inputa = document.getElementById('undebounce')
        const inputb = document.getElementById('debounce')
        const inputc = document.getElementById('throttle')
        
        let debounceAjax = debounce(ajax, 500)
        let throttleAjax = throttle(ajax, 500)
        
        inputa.addEventListener('keyup', function(e) {
            ajax(e.target.value)
        })
        
        inputb.addEventListener('keyup', function(e) {
            debounceAjax(e.target.value)
        })
        
        inputc.addEventListener('keyup', function(e) {
            throttleAjax(e.target.value)
        })
    </script>
</body>
</html>

📚 深入理解闭包

🔒 什么是闭包

闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

💎 闭包的特性

  1. 函数嵌套:一个函数内部定义了另一个函数
  2. 变量引用:内部函数引用了外部函数的变量
  3. 作用域链:内部函数可以访问外部函数的作用域
  4. 内存保持:外部函数的变量不会被垃圾回收

🎯 闭包在防抖/节流中的作用

在防抖和节流的实现中,闭包起到了关键作用:

  1. 保存状态:定时器ID、上次执行时间等状态保存在闭包中
  2. 持久化:即使外部函数执行完毕,状态依然存在
  3. 封装性:状态对外部不可见,只能通过返回的函数访问

⚠️ 闭包的注意事项

  1. 内存占用:闭包会占用内存,因为变量不会被回收
  2. 性能影响:过度使用闭包可能导致内存泄漏
  3. 合理使用:在需要保持状态时使用,不需要时及时释放

🎯 最佳实践

✅ 选择防抖的场景

  • 搜索框输入
  • 表单验证
  • 窗口resize
  • 文本域自动保存

✅ 选择节流的场景

  • 滚动事件
  • 鼠标移动
  • 窗口scroll
  • 动画帧更新

💡 性能优化建议

  1. 合理设置延迟时间:根据实际需求调整delay值
  2. 避免过度优化:不是所有事件都需要防抖/节流
  3. 测试验证:通过控制台日志验证优化效果
  4. 考虑用户体验:在性能和体验之间找到平衡

🚀 总结

闭包是JavaScript中一个强大而重要的特性,它在性能优化方面有着广泛的应用。防抖和节流是闭包应用的典型场景,它们通过巧妙地利用闭包和定时器,有效地解决了事件频繁触发带来的性能问题。

  • 防抖:只执行最后一次,适合搜索建议等场景
  • 节流:固定频率执行,适合滚动事件等场景
  • 闭包:保存状态,实现函数的记忆功能

在实际开发中,合理使用防抖和节流,可以显著提升应用的性能和用户体验。同时,也要注意闭包可能带来的内存问题,做到合理使用,及时释放。

掌握这些技术,将帮助你写出更高效、更优雅的JavaScript代码!🎉