类比
- 防抖:电梯关门。最后一个人进入后,等待一段时间后才关门
- 节流:地铁发车。每隔固定时间发一班车,不等人。
作用
间接影响 TTI(time to interactive
),减少不必要的JS执行,释放主线程 => 加快页面响应。
防抖:通过将【一段时间内】,【高频且耗时的操作】合并为一次执行。
节流:【固定频率】执行操作,稀释执行密度。
应用场景
防抖:【搜索框用户输入联想】【窗口频繁的resize事件】
节流:【滚动事件监听】【动画渲染,保持 16ms 一帧的节奏】【用户滚动相关的一系列事件】
具体实现
防抖(debounce)
javascript
export const debounce = (fn, delay) => {
let timer = null
return function (...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(null, args)
}, delay)
}
}
防抖(立即执行)
增加一个第一次立即执行的标识即可
javascript
export const debounceImmediate = (fn, delay) => {
let timer = null
let firstFlag = true
return function (...args) {
if (firstFlag) {
fn.apply(this, args)
firstFlag = false
return
}
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
节流(throttle)date版本
因为时间比较符合直觉,一般用 date 实现。
javascript
export const throttle = (fn, delay) => {
let lastTime = 0
return function (...args) {
const now = +new Date()
if (now - lastTime >= delay) {
fn.apply(this, args)
lastTime = now
}
}
}
节流(throttle)timer版本
javascript
export const throttleTimer = (fn, delay) => {
let timer = null
return function (...args) {
if (timer) {
return
}
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
节流尾部调用版本
记录用户最后一次滚动常用此版本
除第一次外,都往加 timer
的方向思考(因为除第一次,每一次都有可能是最后一次)
javascript
export const throttleTrailing = (fn, delay, trailing = true) => {
let lastTime = 0
let timer = null
return function (...args) {
const curDate = +new Date()
if (curDate - lastTime >= delay) {
lastTime = curDate
if (timer) {
timer = null
clearTimeout(timer)
}
fn.apply(this, args)
return
}
if (!timer && trailing) {
timer = setTimeout(
() => {
fn.apply(this, args)
timer = null
},
delay - (curDate - lastTime),
)
}
}
}

上述所有函数的测试代码
mocha
+ chai
scss
import {
debounce,
debounceImmediate,
throttle,
throttleTimer,
throttleTrailing,
} from '../src/js/throttle-debounce.js'
import { expect } from 'chai'
import { describe } from 'mocha'
let count = 0
let trailingCount = 0
describe('throttle-debounce', () => {
beforeEach(() => {
count = 0
})
it('debounce normal', (done) => {
const debounceFn = debounce(() => {
++count
}, 100)
debounceFn()
debounceFn()
debounceFn()
setTimeout(() => {
expect(count).to.equal(1)
done()
}, 500)
})
it('debounce immediate', (done) => {
const debounceImmediateFn = debounceImmediate(() => {
++count
}, 100)
debounceImmediateFn()
debounceImmediateFn()
debounceImmediateFn()
setTimeout(() => {
expect(count).to.equal(2)
}, 200)
done()
})
it('throttle normal', (done) => {
const throttleFn = throttle(() => {
++count
}, 300)
throttleFn() // 1
setTimeout(() => {
throttleFn()
setTimeout(() => {
throttleFn() // 2
done()
}, 300)
}, 200)
})
it('throttle timer', (done) => {
const throttleFn = throttleTimer(() => {
++count
}, 300)
throttleFn() // 1
setTimeout(() => {
throttleFn()
setTimeout(() => {
throttleFn() // 2
done()
}, 300)
}, 200)
})
it('throttle trailing', (done) => {
const throttleFn = throttleTrailing(() => {
++trailingCount
}, 300)
throttleFn() // 1
setTimeout(() => {
throttleFn() // 200 ms 之后又进行了执行,应该算是最后一次,理应执行
setTimeout(() => {
expect(trailingCount).to.equal(2)
done()
}, 500)
}, 200)
})
})