防抖、节流、重绘与回流
2019-06-14 15:05:45

写在前面

最近在查漏补缺(其实就是菜),发现一段时间不学习后就会被新的知识所淘汰呢,只有不断地努力学习才能不断地提高自己。

今天我们就来讲一下最近前端的几个新名词:防抖、节流、重绘和回流。虽说是新名词,但并不是新技术。

防抖和节流

概念(定义)

  • 防抖:任务频繁触发的情况下,只有任务触发的间隔超过制定的时间间隔的时候,任务才会被执行。

  • 节流:指定时间间隔内只会执行一次任务。

解释

防抖

这里引用了知乎上的一段示例代码:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>防抖</title>
</head>
<body>
<button id="debounce">点我防抖!</button>

<script>
window.onload = function() {
// 1、获取这个按钮,并绑定事件
var myDebounce = document.getElementById("debounce");
myDebounce.addEventListener("click", debounce(sayDebounce));
}

// 2、防抖功能函数,接受传参
function debounce(fn) {
// 4、创建一个标记用来存放定时器的返回值
let timeout = null;
return function() {
// 5、每次当用户点击/输入的时候,把前一个定时器清除
clearTimeout(timeout);
// 6、然后创建一个新的 setTimeout,
// 这样就能保证点击按钮后的 interval 间隔内
// 如果用户还点击了的话,就不会执行 fn 函数
timeout = setTimeout(() => {
fn();
}, 1000);
};
}

// 3、需要进行防抖的事件处理
function sayDebounce() {
// ... 有些需要防抖的工作,在这里执行
console.log("防抖成功!");
}

</script>
</body>
</html>

我们来理解一下代码:

  1. 我们点击页面上的按钮,执行 debounce函数,并传入参数sayDebounce
  2. 在函数debounce中返回了一个匿名函数,并在匿名函数中定义了一个定时器。
  3. 执行匿名函数时,先清除定时器,然后再创建一个新的定时器。
  4. 延时1秒执行传入的函数sayDebounce()

程序大致的执行过程已经介绍完了,整个防抖程序的关键就在于第3步中。

为什么这么说呢?

因为,我们每次点击都会去执行匿名函数,匿名函数每次执行前都会去清除一次定时器,然后再重新定义定时器。这样做就会让函数永远都只会在满足定时器制定的时间间隔后才会执行。(可能这样说有点不恰当)。我们改造一下这个sayDebounce函数:

1
2
3
4
5
6
function sayDebounce() {
//console.log("防抖成功!");
let myDate = new Date();
let mytime=myDate.toLocaleTimeString();
console.log(myDate.toLocaleString());
}

这样,我们再次执行程序的时候控制台打印出来的时间永远都是我们最后一次点击的时间加上1秒(不考虑程序执行时间)。

举个栗子:

假设,我今天要去抢手机,手机早上11点整开售,我们10点59分50秒进去疯狂点击,但是由于抢购按钮采用了防抖,且时间间隔为1秒,那么无论我们手速多块,一秒点多少次,都是无效的。因为程序根本不会执行到抢购程序那里,所以最好的抢购时机就是10点59分59秒的时候点击,那么11点整的时候正好执行了抢购程序(不考虑程序执行时间)。

节流

同样,这里也是引用了知乎的一个示例代码:

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
45
46
47
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>节流</title>
</head>
<body>

<button id="throttle">点我节流!</button>

<script>
window.onload = function() {
// 1、获取按钮,绑定点击事件
var myThrottle = document.getElementById("throttle");
myThrottle.addEventListener("click", throttle(sayThrottle));
}

// 2、节流函数体
function throttle(fn) {
// 4、通过闭包保存一个标记
let canRun = true;
return function() {
// 5、在函数开头判断标志是否为 true,不为 true 则中断函数
if(!canRun) {
return;
}
// 6、将 canRun 设置为 false,防止执行之前再被执行
canRun = false;
// 7、定时器
setTimeout( () => {
fn();
// 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
canRun = true;
}, 1000);
};
}

// 3、需要节流的事件
function sayThrottle() {
console.log("节流成功!");
}

</script>
</body>
</html>

我们首先来解释一下代码:

  1. 我们点击页面上的按钮,执行 throttle函数,并传入参数sayThrottle
  2. 在函数throttle中定义了一个bool变量canRun,赋值为true;还返回了一个匿名函数。
  3. 在执行匿名函数时,先判断canRun,若值为false,则return,反之赋值canRun赋值为false,然后再创建一个定时器。
  4. 在定时器中延时1秒执行传入的函数sayThrottle(),并将canRun的值赋为true。

程序的大致执行过程已经介绍完了,整个节流程序中核心步骤在第3步与第4步中。

每次我们点击按钮,程序都会去执行匿名函数。在执行匿名函数中的定时器之前,程序会先去判断canRun的值,若值为false,则直接退出程序。反之才会执行定时器。即,程序第一次执行完之前,该程序是无法继续去执行第二次的。我们改造一下这个sayThrottle函数:

1
2
3
4
5
6
function sayThrottle() {
//console.log("节流成功!");
let myDate = new Date();
let mytime=myDate.toLocaleTimeString();
console.log(myDate.toLocaleString());
}

这样,我们再次执行程序的时候在控制台会依次打印出前一次成功执行后第一个点击的时间(有点绕)。比如,我2019-06-14 10:00:00第一次点击,然后我疯狂点击,比如在10:00:00到10:00:01这一秒内我点了100次,但是控制台最终只会打印出:

1
2
2019-06-14 10:00:00
2019-06-14 10:00:01

举个栗子:

跟前面一样。假设,我今天要去抢手机,手机早上11点整开售,我们10点59分50秒进去疯狂点击,但是由于抢购按钮采用了节流,且时间间隔为1秒,那么无论我们手速多块,一秒点多少次,都是无效的。因为程序只会在50秒的时候出发,51秒执行完成,完成后因为我们点的速度很快,程序立马又继续被触发。所以无论我们50-51秒内点击了多少次都不会影响程序的正常执行的(不考虑程序执行时间)。

区别

函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

总结

  • 函数防抖:将多次操作合并为一次操作进行。原理是维护一个计时器,规定在一定时间后触发函数,但是在规定时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
  • 函数节流:使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。
  • 函数防抖属于阻止函数继续执行,函数节流属于稀释函数执行频率

重绘和回流

概念(定义)

  • 重绘(repaint):当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制。

  • 回流(reflow):又叫重排(layout),当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局。

常见操作

  • 重绘:
    • 改变元素颜色
    • 改变元素背景色
    • ……
  • 回流:
    • 页面首次渲染时
    • 浏览器窗口大小改变
    • 元素尺寸、位置和内容发生改变
    • 字体大小的变化
    • 添加或删除DOM元素
    • 激活CSS伪类
    • ……

注意:回流必定会触发重绘,重绘不一定会触发回流。重绘的开销较小,回流的代价较高。

与防抖和节流的关系

为什么需要 节流?它与回流有什么关系?

因为有些事情会造成浏览器的 回流,而 回流 会使浏览器开销增大,所以我们通过 节流 来防止这种增大浏览器开销的事情。

举个栗子:

页面上有个 div 框,用户可以在 input 框中输入 div 框的一些信息,例如宽、高等,输入完毕立即改变属性。但是,因为改变之后还要随时存储到数据库中,所以需要调用接口。如果不加限制,每输入完一个属性后都会造成一次回流和一次接口的调用,这样会大大增加浏览器的开销。

如何减少使用

既然重绘和回流都会增加浏览器的开销,那么我们因该怎样去避免大量使用他们呢?

  • 避免频繁操作样式,可汇总后统一一次修改
  • 尽量使用 class 进行样式修改,而不是直接操作样式
  • 减少 DOM 的操作,可使用字符串一次性插入

做到这三点,就可以大大减少回流与重绘的使用。

上一页
2019-06-14 15:05:45
下一页