手写防抖与节流函数

我们经常会绑定一些持续触发的事件,比如resize、scroll、mousemove等等,如果事件调用无限制,会加重浏览器负担,导致用户体验差,我们可以使用debounce(防抖)和throttle(节流)的方式来减少频繁的调用,同时也不会影响实际的效果。

防抖的概念

触发事件后n秒后才执行函数,如果在n秒内触发了事件,则会重新计算函数执行时间
防抖函数可以分为立即执行,和非立即执行两个版本

  • 非立即执行版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function debounce(fn, wait) {
    let timer = null
    return function () {
    const context = this
    const args = arguments
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
    fn.apply(context, args)
    }, wait)
    }
    }

    此函数一开始不会马上执行,而是等到用户操作结束之后等待wait秒后才执行,如果在wait之内用户又触发了事件,则会重新计算

  • 立即执行版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function debounce(fn, wait) {
    let timer = null
    return function () {
    const args = arguments
    const now = !timer
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
    timer = null
    }, wait)
    if (now) {
    fn.apply(this, args)
    }
    }
    }

    立即执行就是触发事件后马上先执行一次,直到用户停止执行事件等待wait秒后再执行一次

我们可以将两种版本合并成一种

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
/**
* @desc 函数防抖
* @param fn 函数
* @param wait 延迟执行毫秒数
* @param immediate true 表立即执行,false 表示非立即执行
*/
function debounce(fn, wait, immediate) {
let timer = null
return function () {
const context = this
const args = arguments
timer && clearTimeout(timer)
if (immediate) {
const now = !timer
timer = setTimeout(() => {
timer = null
}, wait)
now && fn.apply(context, args)
} else {
timer = setTimeout(() => {
fn.apply(context, args)
}, wait)
}
}
}

节流的概念

连续触发事件但在n秒内只执行一次函数
对于节流有时间戳和定时器两种版本

  • 时间戳版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function throttle(fn, wait) {
    var prev = 0
    return function () {
    let now = Date.now()
    let context = this
    let args = arguments
    if (now - prev > wait) {
    fn.apply(context, args)
    prev = now
    }
    }
    }
    在持续触发事件的过程中,函数会立即执行,用户在wait秒内不管执行多少次事件,都会等待wait秒后再执行。
  • 定时器版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function throttle(fn, wait) {
    var timer = null
    return function () {
    const context = this
    const args = arguments
    if (!timer) {
    timer = setTimeout(() => {
    timer = null
    fn.apply(context, args)
    }, wait)
    }
    }
    }
    在触发事件的过程中,不会立即执行,并且每wait秒执行一次,在停止触发事件后还会再执行一次。

时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。

将两种方式合并

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
/**
* @desc 函数节流
* @param fn 函数
* @param wait 延迟执行毫秒数
* @param type 1 表时间戳版,2 表定时器版
*/
function throttle(fn, wait, type) {
if (type === 1) {
var prev = 0
} else {
var timer = null
}
return function() {
const context = this
const args = arguments
if (type === 2) {
if (!timer) {
timer = setTimeout(() => {
timer = null
fn.apply(context, args)
}, wait)
}
} else if(type === 1) {
const now = Date.now()
if (now - prev > wait) {
fn.apply(context, args)
prev = now
}
}
}
}

参考链接