🚀 性能优化主题
在前端开发中,性能优化是一个永恒的话题。当用户与页面交互时,某些事件可能会被频繁触发,导致大量的计算或网络请求,从而影响页面性能和用户体验。闭包作为JavaScript中一个强大的特性,在性能优化方面有着广泛的应用。
📋 需要优化的场景
🎯 防抖
在处理用户输入时,经常会遇到"帕金森"式的问题:
- 执行太密集:用户快速输入时,每个字符都会触发事件
- 任务比较复杂:每次触发都需要执行耗时的操作,比如网络请求
💡 典型应用场景
-
搜索建议功能
- 百度搜索的ajax suggest
- 代码编辑器的智能提示
- 如果请求太快,服务器开销就大
- 如果请求太慢,用户体验就差
-
窗口resize事件
- 调整浏览器窗口大小时会频繁触发
- 可能需要重新计算布局或发送统计数据
-
滚动事件
- 用户滚动页面时持续触发
- 可能需要加载更多内容或更新UI状态
⏱️ 防抖的原理与实现
🎯 核心概念
防抖的核心思想是:管你keyup触发多少次,我只执行最后一次。
当事件被频繁触发时,防抖函数会确保在指定的时间内,只执行一次真正的处理函数。如果在等待时间内事件再次被触发,则重新计时。
🔧 实现原理
防抖的实现主要借助两个关键技术:
- 定时器:使用
setTimeout推迟执行 - 闭包:将定时器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)
}
}
📝 代码详解
-
高阶函数:
debounce是一个高阶函数,因为它接收函数作为参数,并返回一个新的函数 -
闭包的作用:
id变量定义在debounce函数内部- 返回的内部函数可以访问
id变量 - 即使
debounce函数执行完毕,id变量也不会被垃圾回收 - 这就是闭包的神奇之处
-
执行流程:
- 每次触发事件时,先清除之前的定时器(如果存在)
- 保存当前的
this上下文(因为定时器回调会丢失this) - 设置新的定时器,在延迟后执行真正的函数
- 如果在延迟期间再次触发,会清除之前的定时器,重新计时
-
为什么需要保存
this:- 定时器回调函数中的
this指向全局对象(非严格模式) - 使用
fn.call(that, args)可以确保正确的this指向
- 定时器回调函数中的
🎢 节流的原理与实现
🎯 核心概念
节流的核心思想是:每隔一定时间执行一次。
与防抖不同,节流会确保函数以固定的频率执行,不管事件触发得多么频繁。
🔧 实现原理
节流的实现需要记录上一次执行的时间,并根据当前时间判断是否应该执行:
- 时间戳:记录上次执行的时间
- 延迟定时器:处理边界情况
💻 代码实现
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)
}
}
}
📝 代码详解
-
变量说明:
last:记录上次执行的时间戳deferTimer:延迟定时器IDnow:当前时间戳(使用+new Date()快速转换)
-
执行逻辑:
- 如果是第一次执行(
last为空),直接执行 - 如果距离上次执行时间不足
delay,设置延迟定时器 - 如果距离上次执行时间超过
delay,立即执行
- 如果是第一次执行(
-
为什么需要
deferTimer:- 处理最后一次触发后,可能需要延迟执行的情况
- 确保最后一次操作也能被执行
-
使用
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>
📚 深入理解闭包
🔒 什么是闭包
闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
💎 闭包的特性
- 函数嵌套:一个函数内部定义了另一个函数
- 变量引用:内部函数引用了外部函数的变量
- 作用域链:内部函数可以访问外部函数的作用域
- 内存保持:外部函数的变量不会被垃圾回收
🎯 闭包在防抖/节流中的作用
在防抖和节流的实现中,闭包起到了关键作用:
- 保存状态:定时器ID、上次执行时间等状态保存在闭包中
- 持久化:即使外部函数执行完毕,状态依然存在
- 封装性:状态对外部不可见,只能通过返回的函数访问
⚠️ 闭包的注意事项
- 内存占用:闭包会占用内存,因为变量不会被回收
- 性能影响:过度使用闭包可能导致内存泄漏
- 合理使用:在需要保持状态时使用,不需要时及时释放
🎯 最佳实践
✅ 选择防抖的场景
- 搜索框输入
- 表单验证
- 窗口resize
- 文本域自动保存
✅ 选择节流的场景
- 滚动事件
- 鼠标移动
- 窗口scroll
- 动画帧更新
💡 性能优化建议
- 合理设置延迟时间:根据实际需求调整delay值
- 避免过度优化:不是所有事件都需要防抖/节流
- 测试验证:通过控制台日志验证优化效果
- 考虑用户体验:在性能和体验之间找到平衡
🚀 总结
闭包是JavaScript中一个强大而重要的特性,它在性能优化方面有着广泛的应用。防抖和节流是闭包应用的典型场景,它们通过巧妙地利用闭包和定时器,有效地解决了事件频繁触发带来的性能问题。
- 防抖:只执行最后一次,适合搜索建议等场景
- 节流:固定频率执行,适合滚动事件等场景
- 闭包:保存状态,实现函数的记忆功能
在实际开发中,合理使用防抖和节流,可以显著提升应用的性能和用户体验。同时,也要注意闭包可能带来的内存问题,做到合理使用,及时释放。
掌握这些技术,将帮助你写出更高效、更优雅的JavaScript代码!🎉