JavaScript中的节流与防抖

0、前言

之前经常在掘金的推荐文章里面看到函数防抖和函数节流相关的文章,看过之后一直没实际操作过,自然而然的也就忘了这回事。

~~~今天在写项目时,遇到一个问题子组件监听(watch)了父组件传来的mode(是编辑还是详情)以及id(每个列表的id)来获取列表详情,可是每次都会调两遍接口。于是Google了一翻,重新了解了一下函数节流。~~~
其实是不需要监听’mode’和’id’的,只需要监听模态框的值即’visible’就可以。之前的思维僵化了…不过了解一下防抖和节流对开发也有很多帮助。

1、函数节流(throttle)

1.1 概念

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

举个🌰:登录页面用户输完账户密码后点击登录按钮,如果ajax请求很慢然后button也没有disabled用户看到还没有跳转进首页就会烦躁的频繁点击登录按钮,然后频繁的发送ajax请求造成资源浪费。
这个时候就需要函数节流了,在一定时间间隔内例如3秒,用户无论点击多少次登录按钮都只会调用一次登录接口,3秒过后再次点击的话才会再次调用。同理一些商城的秒杀也是用到了节流(用户会疯狂点击)。

上面的🌰不太好理解的话再来一个:最近很火的游戏刺激战场中的自动步枪M416,无论你单身多少年,手速多块,每秒中射出的子弹的数量始终是固定的,不会因为你的手速而影响。而节流的原理就和这个类似(固定时间只执行一次,不会因为多次点击而影响)。

1.2 原理

使用一个阀门,等执行完再打开。

1.3 使用场景

  1. 重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动(mousemove)等频繁触发的函数
  2. 用户短时间内多次快速点击

使用函数节流可以降低触发频率从而降低计算的频率,而不必去浪费资源。

1.4 代码实现

1.4.1 利用时间戳

es5版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function throttle(fn, delay) {
// fn是指需要节流的函数,delay是多长时间内再次触发
let last = +Date.now();
return function () {
let _args = arguments;
let _self = this;
let now = +Date.now();
console.log(`时间过去了${now - last}毫秒`);
if (now - last >= delay) {
fn.apply(_self, _args);
last = +Date.now();
}
}
}

// 调用
document.querySelector('#login').addEventListener('click', throttle(login, 3000));

点这里看利用时间戳实现的节流demo

es6版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const throttle = (fn, awaitTime) => {
let last = Date.now();
return (...args) => {
const now = Date.now();
if (now - last >= awaitTime) {
fn(...args);
last = now;
}
}
}
// 调用
const throttleScroll = throttle(test, 3000);
window.onscroll = function () {
throttleScroll('1', '呆呆', 3, 'asd');
}
// 需要节流的函数
function test(a, b, c, d) {
console.log(a, b, c, d)
}

点这里看利用时间戳实现的节流demo(es6版)

1.4.3 利用定时器

es5版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(fn, delay) {
let timer = null;
return function () {
let _args = arguments;
let _self = this;
if (!timer) {
timer = setTimeout(function () {
fn.apply(_self, _args)
timer = null;
}, delay)
}
}
}

// 调用方法同上

点这里看利用定时器实现的节流demo

es6版本

1
2
3
4
5
6
7
8
9
10
11
const throttle = (fn, awaitTime) => {
let timer = null;
return (...args) => {
if (!timer) {
timer = setTimeout(() => {
fn(...args);
timer = null;
}, awaitTime)
}
}
}
1.4.4 综合版本

使用时间戳或者定时器都是可以实现节流的,但是有个缺点,就是第一次执行的时候没有立即执行而是过了一段时间。我们来优化一下代码

es5版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function throttle(fn, delay) {
let timer, last;
return function () {
let _args = arguments;
let _self = this;
let now = +Date.now();
console.log(`时间过去了${now - last}毫秒`);
if (last && now < last + delay) {
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(_self, _args);
last = now;
}, delay)
} else {
last = now;
fn.apply(_self, _args);
}
}
}

// 调用方法同上

点这里看时间戳和定时器综合版本的节流demo

es6版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const throttle = (fn, awaitTime) => {
let timer, last ;
return (...args) => {
const now = Date.now();
if (last && now - last <= awaitTime) {
window.clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
last = now;
}, awaitTime)
} else {
// 如果没有last说明是第一次触发,允许其立即执行
fn(...args);
last = now;
}
}
}

2、函数防抖(debounce)

2.1 概念

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

举个🌰:远程搜索(联想输入)时,如果监听keyup事件用户每次按下键盘都会触发一次ajax请求,这样效率极低,使用防抖的话,当最后一次按下键盘时才会执行ajax请求。百度的搜索框:当停止连续输入后就会帮你展示一些联想词。也就用到了防抖。

来个通俗易懂的🌰:如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

2.2 原理

在执行setTimeout之前,先使用clearTimeout()把上一次定时器给清除掉,这样就达到了只会执行最后一次触发的setTimeout

2.3 使用场景

  1. 重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动(mousemove),键盘事件(keyup)等频繁触发的函数
  2. 远程搜索

机智的小伙伴们有没有发现其实防抖和节流的使用场景是有重叠部分的,其实主要还是看业务需求来选择。

2.4 代码实现

2.4.1 利用定时器

es5版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function debounce(func, wait) {
let timeout;
return function () {
let _self = this;
let _args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function () {
func.apply(_self, _args)
}, wait);
}
}

// 调用
document.querySelector('#container').addEventListener('mousemove', debounce(getUserAction, 1000))

es6版本

1
2
3
4
5
6
7
8
9
const debounce = (fn, awaitTime) => {
let timer;
return (...args) => {
window.clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, awaitTime)
}
}

点这里看利用时间戳实现的防抖的demo

2.4.3 优化版本

和之前的节流一样,我们发现第一次并没有立即执行,也是等待停止触发事件后才执行,这样明显用户体验不好,我们也来优化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const debounce = (fn, awaitTime) => {
let timer, immediate = true // immediate用来标记是否第一次执行,第一次则执行,true为第一次;
return (...args) => {
window.clearTimeout(timer)
if (immediate) {
fn(...args);
immediate = false;
} else {
timer = setTimeout(() => {
fn(...args);
}, awaitTime);
}
}
}

3、总结

我们来总结一下之前学到的东西:

  1. 无论是防抖还是节流都使用了闭包的写法,这样写是为了存储setTimeout状态或者last值

  2. es6版本比es5版本精简了很多,主要是this和arguments不需要手动改变指向


JavaScript中的节流与防抖
https://xypecho.github.io/2019/08/08/JavaScript的函数节流/
作者
很青的青蛙
发布于
2019年8月8日
许可协议