写在最前面:这是我即将开始写的一个系列,主要是在框架横行的时代,虽然上班用的是框架,但是对于面试,以及技术进阶,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
函数被疯狂执行,接下来,我们来引入今天的主角,防抖 和 节流
![e831ff74f205b600384e9feff0509a2b.png](https://i-blog.csdnimg.cn/blog_migrate/813cddf87ba0bce0cfbd6ac9f2dab456.png)
防抖
定义
将多次执行变为最后一次执行或执行,你可以理解为防止手抖
使用场景
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染
实现方式
- 非立即执行版
这应该是最基础也是最常用的一个版本,先来看下代码
function debounce(func,wait) {
let timeout; //延时器变量
return function () {
const context = this; //改变this指向
const args = [...arguments]; //函数入参,另外使用了...扩展运算符
if (timeout) clearTimeout(timeout); //先判断有没有延时器,有则清空,毕竟要最后一次执行
timeout = setTimeout(() => {
func.apply(context, args) //apply调用传入方法
}, wait);
}
}
可以看到,方法 debounce()
有两个入参,一个方法名 func
, 以及一个延时时间 wait
,单位 ms
。
接下来,使用 content.onmousemove = debounce(count,1000);
调用我们新写的非立即执行版的防抖,先来看下实际的运行效果,可以看到事件触发了以后,只有在触发以后的1s内不再触发,才会执行相应的方法,也就是 count++
。如果停止时间间隔小于 wait
的值并且再次触发,那么将重新计算执行时间,计时器结束以后,再执行方法。总结而言就是触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间,也就是方法的执行是非立即执行的
![d5d30f79e7e65c043a5f58482882193b.png](https://i-blog.csdnimg.cn/blog_migrate/8fae3f6c401f2b03505c6ef35c377a93.png)
整个方法的核心思想就是依靠变量 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 秒内不触发事件才能继续执行函数的效果
![1b2b2c441c28fe7fa946e834f014e499.png](https://i-blog.csdnimg.cn/blog_migrate/f09d505d30255cf84139fbbcec13feb2.png)
节流
定义
将多次执行变为每隔一段时间执行一次
使用场景
- 滚动加载,加载更多或滚到底部监听
实现方式
- 时间戳版(立即执行版)
在持续触发事件的过程中,函数会立即执行,并且每隔一段时间执行一次,代码如下:
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一直在实时更新,时间长了,条件肯定会成立的)
⑤ 以此类推,完成了事件一直在触发,首次立即执行函数,之后函数只会隔一段时间执行
分析完了代码,让我们来看看实际运行效果,果然和我们的分析如出一辙:
![d9770881462eafd8ad7a22b7fb706aa6.png](https://i-blog.csdnimg.cn/blog_migrate/23e3b457f7a6761337d53ae99b204ef1.png)
- 延时器版(非立即执行版)
在持续触发事件的过程中,函数不会立即执行,并且每隔一段时间执行一次,在停止触发事件后,函数还会再执行一次,代码如下:
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)
条件成立,继续创建延时器
⑤ 以此类推, 有延时器就什么都不做,没有了延时器则创建
⑥ 即使不触发事件,延时器仍然存在,所以, 停止触发事件以后,函数仍然会再执行一次
分析完了代码,让我们来看看实际运行效果,果然和我们的分析如出一辙:
![149e536b050aee2c96803334fbf86d0c.png](https://i-blog.csdnimg.cn/blog_migrate/6e68855ae3cf26c55e39512b7ef5f452.png)
系列目录
- 一文搞懂JS系列(一)之编译原理,作用域,作用域链,变量提升,暂时性死区
- 一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝
- 一文搞懂JS系列(三)之垃圾回收机制,内存泄漏,闭包
- 一文搞懂JS系列(四)之闭包应用-柯里化,偏函数
- 一文搞懂JS系列(四)之闭包应用-防抖,节流