手写防抖、节流函数

防抖和节流的提出是为了解决由于某些频繁事情的发生导致对系统性能的损耗。

手写防抖函数

查看

对于防抖的实现过程为:

  • 当触发一个事件时,相应的函数并不会立刻执行,而是会等待一定时间再执行。

  • 当频繁触发一个事件的,相应的函数将会被频繁的推迟。

  • 当停止触发该事件,且在等待时间内,不再触发该事件,其相应的函数才会执行一次

在JS中防抖的应用场景包括:

  • 对输入框的的输入监听。

  • 对DOM(按钮等)的频繁操作。

  • 对浏览器滚动条的事件监听。

  • 对浏览器缩放resize的监听。

防抖函数的基础代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<input type="text" class="input">
<script>
// 核心代码
function debounce(fn, delay) {
let timer = null
function _debounce() {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
}, delay)
}
return _debounce
}

const inputEl = document.querySelector('.input')

inputEl.oninput = debounce(() => {
console.log(inputEl.value)
}, 1000)
</script>

接下来对其进行优化加入更多要求:

优化参数和this指向

1
2
3
4
5
6
7
8
// 由于实现防抖的是_debounce函数,所以传递的参数都在_debounce上
// 且根据箭头函数不存在this,根据作用链查找的特性,将定时器的回调函数改为箭头函数就能实现传递参数和this指向正确绑定
function _debounce(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}

优化取消操作(增加取消功能)

1
2
3
4
5
6
// 将定时器挂载在返回的防抖函数对象上,用户可以通过调用该函数的cancel方法来清除定时器停止调用防抖对应的函数
_debounce.cancel = function () {
clearTimeout(timer)
// 保持状态一致性,避免冗余的无意义操作,因为虽然通过clearTimeout清除了定时器,但timer仍保留对应的定时器ID,设置为null既可以保证避免下一次进行冗余的判断操作,也有益于垃圾回收的回收。
timer = null
}

优化立即执行效果(第一次立即执行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 为工具函数添加了一个参数,用该参数判断是否要进行第一次立即执行
function debounce(fn, delay, immediate = true) {
let timer = null
// 这里多定义一个变量而不直接操作immediate是为了保持封装性,避免了直接修改外部的状态,也避免了可能的错误
let isInvoke = false
function _debounce(...args) {

if (timer) clearTimeout(timer)

if (!isInvoke && immediate) {
fn.apply(this, args)
isInvoke = true
return
}

timer = setTimeout(() => {
fn.apply(this, args)
isInvoke = false
timer = null
}, delay)
}
_debounce.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
}
return _debounce
}

const inputEl = document.querySelector('.input')
const btnEl = document.querySelector('.btn')
const debounceFn = debounce((event) => {
console.log(event.target.value)
}, 1000)

inputEl.oninput = debounceFn
btnEl.onclick = debounceFn.cancel

优化防抖返回值

对于获取其返回值有两种方法,第一种为传入一个回调函数,通过回调函数获取返回值,第二种方式则是通过使用Promise来获取返回值。这里展示第二种方案。

如果有其他定制化操作,也可自行添加,但核心也就是下面这样了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function debounce(fn, delay, immediate = true) {
let timer = null
let isInvoke = false
function _debounce(...args) {

return new Promise((resolve, reject) => {
try {
if (timer) clearTimeout(timer)

if (!isInvoke && immediate) {
const result = fn.apply(this, args)
resolve(result)
isInvoke = true
return
}

timer = setTimeout(() => {
const result = fn.apply(this, args)
resolve(result)
isInvoke = false
timer = null
}, delay)
} catch (error) {
reject(error)
}
})
}
_debounce.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
}
return _debounce
}

手写节流函数

查看

对于节流的实现过程为:

  • 当事件触发时,会执行这个事件的响应函数。

  • 如果这个事件被频繁出发,那么节流函数会按照一定的频率来执行函数。

  • 不论在这个期间触发了多少次这个事件,执行函数的频率总是固定的。

对于节流的引用场景包括:

  • 监听页面滚动事件。

  • 鼠标移动事件。

  • 用户频繁点击按钮操作。

  • 游戏中的一些设计(如飞机的子弹发射)。

节流函数的基础代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function throttle(fn, delay) {
let satrtTime = 0
function _throttle() {
let nowTime = Date.now()
let waitTime = delay - (nowTime - satrtTime)
if (waitTime <= 0) {
fn()
satrtTime = nowTime
}
}

return _throttle
}

const inputEl = document.querySelector('.input')
const btnEl = document.querySelector('.btn')
const throttleFn = throttle((event) => {
console.log(inputEl.value)
}, 1000)

inputEl.oninput = throttleFn

优化参数、this指向、节流第一次是否可以执行和最后一次可以执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function throttle(fn, interval, { leading = true, trailing = false } = {}) {
let startTime = 0
let timer = null
function _throttle(...args) {

let nowTime = Date.now()
if (!leading && startTime === 0) {
startTime = nowTime
}
let waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
if (timer) clearTimeout(timer)
fn.apply(this, args)
startTime = nowTime
timer = null
return
}

// 判断最后一次是否可以执行
if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this, args)
startTime = Date.now()
timer = null
}, waitTime)
}

}

return _throttle
}

添加取消功能(取消最后一次执行)

1
2
3
4
5
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
startTime = 0
timer = null
}

节流返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function throttle(fn, interval, { leading = false, trailing = true } = {}) {
let startTime = 0
let timer = null
function _throttle(...args) {
return new Promise((resolve, reject) => {
try {
let nowTime = Date.now()
if (!leading && startTime === 0) {
startTime = nowTime
}
let waitTime = interval - (nowTime - startTime)
if (waitTime <= 0) {
if (timer) clearTimeout(timer)
const result = fn.apply(this, args)
resolve(result)
startTime = nowTime
timer = null
return
}

if (trailing && !timer) {
timer = setTimeout(() => {
const result = fn.apply(this, args)
resolve(result)
startTime = Date.now()
timer = null
}, waitTime)
}

} catch (error) {
reject(error)
}
})


}
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
startTime = 0
timer = null
}

return _throttle
}