防抖、节流

284 阅读6分钟

防抖(频繁触发只执行一次)

定义:就是将一段时间内连续的多次触发转化为一次触发。

在无事件触发后的设定时间执行事件,将几次操作合并为一此操作进行。这样一来,只有最后一次操作能被触发。

使用环境:

1,搜索框搜索内容

2, 页面滚动特定距离 显示【返回顶部按钮】

3, 页面大小resize 触发事件

设计思路:

在setTimeout中调用事件处理函数,如果在定时器触发函数执行之前又触发函数,清除定时器。

即只要触发,就会清除上一个计时器,又注册新的一个计时器。直到停止触发 wait 时间后,才会执行回调函数。

不断触发事件,就会不断重复这个过程,达到防止目标函数过于频繁的调用的目的。

以最后一次触发为标准:
/**
 * 实现函数的防抖(目的是频繁触发中只执行一次)
 * @param {*} func 需要执行的函数
 * @param {*} wait 检测防抖的间隔频率
 * @param {*} immediate 是否是立即执行  True:第一次,默认False:最后一次
 * @return {可被调用执行的函数}
 */
function debounce(func, wati = 500, immediate = false) {
  let timer = null
  return function anonymous(... params) {

    clearTimeout(timer)
    timer = setTimeout(_ => {
      // 在下一个500ms 执行func之前,将timer = null
      //(因为clearInterval只能清除定时器,但timer还有值)
      // 为了确保后续每一次执行都和最初结果一样,赋值为null
      // 也可以通过 timer 是否 为 null 是否有定时器
      timer = null
      func.call(this, ...params)
    }, wait)

  }
}  
以第一次触发为标准:
/**
 * 实现函数的防抖(目的是频繁触发中只执行一次)
 * @param {*} func 需要执行的函数
 * @param {*} wait 检测防抖的间隔频率
 * @param {*} immediate 是否是立即执行 True:第一次,默认False:最后一次
 * @return {可被调用执行的函数}
 */
function debounce(func, wait = 500, immediate = true) {
  let timer = null
  return function anonymous(... params) {

    // 第一点击 没有设置过任何定时器 timer就要为 null
    let now = immediate && !timer
    clearTimeout(timer)
    timer = setTimeout(_ => {
      // 在下一个500ms 执行func之前,将timer = null
      //(因为clearInterval只能在系统内清除定时器,但timer还有值)
      // 为了确保后续每一次执行都和最初结果一样,赋值为null
      // 也可以通过 timer 是否 为 null 是否有定时器
      timer = null!immediate ? func.call(this, ...params) : null
    }, wait)
    now ? func.call(this, ...params) : null

  }
}

function func() {
  console. log('ok')
}
btn. onclick = debounce(func, 500)
防抖总结
/*
  func 执行的函数
  wait 需要等待的时间
  immediate 参数判断是否是立刻执行
*/
function debounce(func, wait, immediate) {
    let time
    let debounced = function() {
        let context = this
        if(time) clearTimeout(time)

        if(immediate) {
            let callNow = !time
            if(callNow) func.apply(context, arguments)
            time = setTimeout(
                ()=>{time = null} //见注解
            , wait)
        } else {
            time = setTimeout(
                ()=>{func.apply(context, arguments)}
            , wait) 
        }
    }

    debounced.cancel = function() {
        clearTimeout(time)
        time = null
    }

    return debounced
}

如何使用解析

// 定义一个函数
 function getUserAction(e) {
   ....
};
var setUseAction = debounce(getUserAction, 10000, true);

节流(频繁触发中缩减频率)

定义:减少一段时间内触发的频率(固定时间做某一件事)

使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

使用环境:

1 连续点击按钮 切 需求为 间隔请求 例如 页面的【刷新】按钮

2 上拉加载获取数据 的【上拉加载】

方法:

时间戳

思路:

初始化时获取时间,每次触发事件时再次获取时间,两次时间间隔等于或大于设定时间,执行事件,初始化时间重置为当前时间,如此循环。

  • 如果时间差大于了规定的等待时间,就可以执行一次;目标函数执行以后,就更新 previous 值,确保它是“上一次”的时间。
  • 否则就等下一次触发时继续比较。
// 时间戳
var throttle = function(func, delay) {            
  var prev = Date.now();            
  return function() {                
    var context = this;                
    var args = arguments;                
    var now = Date.now();                
    if (now - prev >= delay) {                    
      func.apply(context, args);                    
      prev = Date.now();                
    }            
  }        
}        
function handle() {            
  console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));
定时器

设计思路:定时器在延时时间执行过后,重置为null, 定时器为null时,重新设置定时器,如此循环。

  • 当定时器不存在,说明可以执行函数,于是定义一个定时器来向任务队列注册目标函数 目标函数执行后设置保存定时器ID变量为空
  • 当定时器已经被定义,说明已经在等待过程中。则等待下次触发事件时再进行查看。
// 节流throttle代码(定时器):
var throttle = function(func, delay) {            
    var timer = null;            
    return function() {                
        var context = this;               
        var args = arguments;                
        if (!timer) {                    
            timer = setTimeout(function() {                        
                func.apply(context, args);                        
                timer = null;                    
            }, delay);                
        }            
    }        
}        
function handle() {            
    console.log(Math.random());        
}        
window.addEventListener('scroll', throttle(handle, 1000));
时间戳和定时器的对比
  • 时间戳实现的:先执行目标函数,后等待规定的时间段;
  • 计时器实现的:先等待够规定时间,再执行。 即停止触发后,若定时器已经在任务队列里注册了目标函数,它也会执行最后一次。
时间戳+定时器的结合
// 节流throttle代码(时间戳+定时器)
/*
  func 执行的函数
  wait 等待的时间
  options  设置两种类型:
          leading:false 表示禁用第一次执行
          trailing: false 表示禁用停止触发的回调
*/
function throttle(func, wait, options) {
  let time, context, args, result;
  let previous = 0;
  if (!options) options = {};

  let later = function() {
    previous = options.leading === false ? 0 : new Date().getTime();
    time = null;
    func.apply(context, args);
    if (!time) context = args = null;
  };

  let throttled = function() {
    let now = new Date().getTime();
    if (!previous && options.leading === false) previous = now;
    let remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      if (time) {
        clearTimeout(time);
        time = null;
      }
      previous = now;
      func.apply(context, args);
      if (!time) context = args = null;
    } else if (!time && options.trailing !== false) {
      time = setTimeout(later, remaining);
    }
  };
  return throttled;
}

如何使用

fucntion action (){
  ....
}
var setAction = throttled(action,1000,{trailing: false})

注意:leading:false 和 trailing: false 不能同时设置。

节流总结代码

【第一次触发:reamining是负数,previous被赋值为当前时间】 【第二次触发:假设时间间隔是500ms,第一次执行完之后,20ms之后,立即触发第二次,则remaining = 500 - ( 新的当前时间 - 上一次触发时间 ) = 500 - 20 = 480 】

/**
 * 实现函数的节流 (目的是频繁触发中缩减频率)
 * @param {*} func 需要执行的函数
 * @param {*} wait 检测节流的间隔频率
 * @param {*} immediate 是否是立即执行 True:第一次,默认False:最后一次
 * @return {可被调用执行的函数}
 */
function throttle(func, wait) {
    let timer = null
  let previous = 0  // 记录上一次操作的时间点

  return function anonymous(... params) {
    let now = new Date()  // 当前操作的时间点
    remaining = wait - (now - previous) // 剩下的时间
    if (remaining <= 0) {
      // 两次间隔时间超过频率,把方法执行
      
      clearTimeout(timer); // clearTimeout是从系统中清除定时器,但timer值不会变为null
      timer = null; // 后续可以通过判断 timer是否为null,而判断是否有 定时器
      
      // 此时已经执行func 函数,应该将上次触发函数的时间点 = 现在触发的时间点 new Date()
      previous = new Date(); // 把上一次操作时间修改为当前时间
      func.call(this, ...params);
    } else if(!timer){ 
      
      // 两次间隔的事件没有超过频率,说明还没有达到触发标准,设置定时器等待即可(还差多久等多久)
      // 假设事件间隔为500ms,第一次执行完之后,20ms后再次点击执行,则剩余 480ms,就能等待480ms
      timer = setTimeout( _ => {
        clearTimeout(timer)
        timer = null // 确保每次执行完的时候,timer 都清 0,回到初始状态
        
        //过了remaining时间后,才去执行func,所以previous不能等于初始时的 now
        previous = new Date(); // 把上一次操作时间修改为当前时间
        func.call(this, ...params);
      }, remaining)
    }
  }
}

function func() {
  console. log('ok')
}
btn. onclick = throttle(func, 500)