在最近,小米9卖的特别火,在官方抢购的时候基本都是一点既空。这不禁让我想到了,官网是怎样控制顾客不停点击发起请求而不导致官网崩溃的呢?这由此引出了前端性能优化中的----防抖和节流。在闲聊完后你就会发现有些时候在抢购商品的时候,你用鼠标在几秒钟不停的按了数十次,或许它仅仅是发送了你第一次点击抢购的那个请求。所以说 抢购时间内的第一次点击尤为关键!
下面来介绍一下什么是防抖!
防抖:任务频繁触发的情况下,只有任务触发的间隔超过制定的时间间隔的时候,任务才会被执行。
下面引用一下知乎上的一个例子:
<!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.call(this, arguments);
}, 1000);
};
}
// 3、需要进行防抖的事件处理
function sayDebounce() {
// ... 有些需要防抖的工作,在这里执行
console.log("防抖成功!");
}
</script>
</body>
</html>
这是知乎上的一个例子,创建一个定时器,如果在规定时间内重复触发该事件,就会调用clearTimeout清除掉上一个定时器,重置定时器。也就是说,这件事本来就是需要等待的,并非立即执行的,如果用户反复点击,那只好重新等待了。所以,fn.call(this, arguments)
其实是将不确定变量替换到函数中了。在这之前已经聊过了arguments,可以看下我之前写得 闲聊js中的apply、call和arguments
在上面的这个例子中是非立即执行版。当然,我把知乎的例子修改一下它,将它变成立即执行版。
<!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; //5.创建一个判断是否可点击值 let doit = true; return function() { // 5、当doit为真,既用户重复点击时,清除定时器 if(doit)clearTimeout(timeout); //6.当doit为false时,既用户可点击,再将doit设为true,防止用户重复点击 else{ fn(); doit = true; } //7.设置定时器,这样就能保证点击按钮后的 interval 间隔内 // 如果用户还点击了的话,就不会执行 将doit设为false函数 timeout = setTimeout(() => { doit = false; }, 1000); }; } // 3、需要进行防抖的事件处理 function sayDebounce() { // ... 有些需要防抖的工作,在这里执行 console.log("防抖成功!"); } </script> </body> </html>
其原理和立即执行版本一样,只是点击执行的先后顺序不同。
下面 我们来聊一下什么是节流吧!
节流:指定时间间隔内只会执行一次任务。
这有点像我们刷抢购一样,当我们在某段时间间隔内触发了多次事件,其实,它只执行一次请求!
下面 我们再来引用知乎的一个例子就会明白了!
<!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.call(this, arguments); // 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true canRun = true; }, 1000); }; } // 3、需要节流的事件 function sayThrottle() { console.log("节流成功!"); } </script> </body> </html>
从这个例子可以看出,节流可以防止在某时间间隔内重复发送请求!其和防抖有点相似,但其有本质的区别,虽然都是防止重复触发事件!
防抖是需要等待多久时间才能再触发一次事件!
节流是多久时间内只能触发一次事件!
重绘与回流
在介绍重绘和回流之前,最好先了解一下浏览器是如何解析解析URL的,或者看一下《浏览器渲染页面过程剖析》
好!现在我们进入正题 !什么是重绘和回流!
重绘(repaint):当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要 UI 层面的重新像素绘制,因此损耗较少。
回流(reflow):又叫重排(layout)。当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。
或许这概念比较抽象,讲起来很难理解!简单点说,就比如我们页面中的某些颜色会发生动态改变,而木有影响到尺寸,布局、位置、结构这些改变的,就叫做重绘,而例如动态添加结点、改变尺寸、位置这些的,就叫做回流!
回流的损耗是比较大的!所以尽量不要产生太多的回流!就比如,样式的动态修改不要多步而尽量应一步到位!
为了避免大量的重绘和回流!
- 避免频繁操作样式,可汇总后统一一次修改
- 尽量使用 class 进行样式修改,而不是直接操作样式
- 减少 DOM 的操作,可使用字符串一次性插入
回流必定会触发重绘,重绘不一定会触发回流。重绘的开销较小,回流的代价较高。