一文搞懂JS系列(五)之闭包应用-防抖,节流

2,496 阅读7分钟

大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂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 执行完以后, callNowtrue ,再设置一个延时器,然后直接执行方法,这就是所谓的立即执行

② 第二次的时候在进入的时候, if (timeout) 为真,将定时器进行清空,callNow=!timeout 为假,条件不成立

if(callNow) 不成立,函数不执行,因为 timeout = null ,往后将不再执行函数,直到延时器完成调用 timeout = null 之后再触发事件

④ 触发之后,timeout = nullcallNow 赋值为真,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) 条件成立,继续创建延时器

⑤ 以此类推,有延时器就什么都不做,没有了延时器则创建

⑥ 即使不触发事件,延时器仍然存在,所以,停止触发事件以后,函数仍然会再执行一次

分析完了代码,让我们来看看实际运行效果,果然和我们的分析如出一辙:

系列目录