写在最前面:这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!
合集地址:一文搞懂JS系列专题
概览
-
食用时间: 10-15分钟
-
难度: 简单,别跑,看完再走
-
食用价值: JS性能优化
-
食材
先来看一段代码,这会是一个贯穿全文的案例,代码如下:
<div id="content" style="height:150px;line-height:150px;
text-align:center; color: #fff;background-color:black;
font-size:80px;"></div>
<script>
let num = 1;
const content = document.getElementById('content');
function count() {
content.innerHTML = num++;
};
content.onmousemove = count;
</script>
可以看到,在黑色色块中移动的同时, addCount
函数被疯狂执行,但是很多时候,我们不希望这个函数执行地如此频繁,毕竟会影响程序或者网页的性能,作为 性能优化
方案的一种,接下来,我们来引入今天的主角,防抖 和 节流
防抖
定义
将多次执行变为最后一次执行或立即执行,你可以理解为防止手抖
使用场景
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染
实现方式
-
非立即执行版
这应该是最基础也是最常用的一个版本,先来看下代码
function debounce(func,wait,...args) {
let timeout; //延时器变量
return function () {
const context = this; //改变this指向
if (timeout) clearTimeout(timeout); //先判断有没有延时器,有则清空,毕竟要最后一次执行
timeout = setTimeout(() => {
func.apply(context, args) //apply调用传入方法
}, wait);
}
}
可以看到,方法
debounce()
有两个入参,一个方法名func
, 以及一个延时时间wait
,单位ms
,还有一个使用了扩展运算符...
的函数执行时候的入参args
(选传)
接下来,使用 content.onmousemove = debounce(count,1000);
调用我们新写的非立即执行版的防抖,先来看下实际的运行效果,可以看到事件触发了以后,只有在触发以后的1s内不再触发,才会执行相应的方法,也就是 count++
。如果停止时间间隔小于 wait
的值并且再次触发,那么将重新计算执行时间,计时器结束以后,再执行方法。总结而言就是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间,也就是方法的执行是非立即执行的
整个方法的核心思想就是依靠变量 timeout
,用来控制当前是否存在定时器,如果有,则清空,清空完以后再继续创建一个。所以,在多次执行的同时,不断清空再新建,直到停止执行以后,在停止执行以后的 wait
毫秒以后,延时器就会成功生效,方法就会被触发,也就是所谓的非立即执行,毕竟,还要等待延时器的延时 wait
。
-
立即执行版
立即执行版就是在触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果,代码如下:
function debounce(func,wait,...args){
let timeout; //延时器变量
return function(){
const context = this;
if (timeout) clearTimeout(timeout);
let callNow = !timeout; //是否立即执行
timeout = setTimeout(() => {
timeout = null;
},wait)
if(callNow) func.apply(context,args)
}
}
可以看到的是, timeout
依然是延时器,主要核心控制是靠 callNow
① 在刚初始化的时候,没有定时器,所以刚开始
callNow=!timeout
执行完以后,callNow
为true
,再设置一个延时器,然后直接执行方法,这就是所谓的立即执行
② 第二次的时候在进入的时候,
if (timeout)
为真,将定时器进行清空,callNow=!timeout
为假,条件不成立
③
if(callNow)
不成立,函数不执行,因为timeout = null
,往后将不再执行函数,直到延时器完成调用timeout = null
之后再触发事件
④ 触发之后,
timeout = null
,callNow
赋值为真,if(callNow)
条件再次符合,完成执行函数
关于上面有一点, clearTimeout(timeout)
以后,console.log(timeout)
输出为 1
不相信的可以看一下下面的代码输出
let timer=setTimeout(()=>{
},1000);
clearTimeout(timer);
console.log(!timer); //false
最后,让我们再来看一下实际使用效果,可以看到的是,触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果
节流
定义
将多次执行变为每隔一段时间执行一次
使用场景
- 滚动加载,加载更多或滚到底部监听
实现方式
-
时间戳版(立即执行版)
在持续触发事件的过程中,函数会立即执行,并且每隔一段时间执行一次,代码如下:
function throttle(func, wait, ...args){
let pre = 0;
return function(){
const context = this;
let now = Date.now();
if (now - pre >= wait){
func.apply(context, args);
pre = Date.now();
}
}
}
① 首先定义了一个只有完成函数调用才更新当前时间的变量
pre
,然后定义了一个实时更新的当前时间now
② 进入第一次计算时间间隔,
now - pre >= wait
是必定成立的,所以函数会立即触发
③ 触发完了以后,将
pre
的值进行更新,之后,now
的值会进行实时更新
④ 直到
now - pre >= wait
的条件成立,也就是现在的时间距离上次触发的时间大于等于wait
的等待时间,函数会再次触发,(毕竟只要函数不触发,pre
的值不更新,而now一直在实时更新,时间长了,条件肯定会成立的)
⑤ 以此类推,完成了事件一直在触发,首次立即执行函数,之后函数只会隔一段时间执行
分析完了代码,让我们来看看实际运行效果,果然和我们的分析如出一辙:
-
延时器版(非立即执行版)
在持续触发事件的过程中,函数不会立即执行,并且每隔一段时间执行一次,在停止触发事件后,函数还会再执行一次,代码如下:
function throttle(func, wait, ...args){
let timeout;
return function(){
const context = this;
if(!timeout){
timeout = setTimeout(() => {
timeout = null;
func.apply(context,args);
},wait)
}
}
}
① 首先定义了一个延时器变量
timeout
,先判断是否有延时器,没有则创建,所以第一次进入函数的时候,会先创建一个延时器
② 再次进入函数的时候,因为当前已经存在延时器了,所以什么都不做
③ 什么都不做直到延时器的时间结束,函数开始执行,将
timeout
进行清空并且执行函数
④ 清空以后,再一次判断,
if(!timeout)
条件成立,继续创建延时器
⑤ 以此类推,有延时器就什么都不做,没有了延时器则创建
⑥ 即使不触发事件,延时器仍然存在,所以,停止触发事件以后,函数仍然会再执行一次
分析完了代码,让我们来看看实际运行效果,果然和我们的分析如出一辙: