重新学防抖debounce和节流throttle,及react hook模式中防抖节流的实现方式和注意事项

重学节流和防抖

防抖

概念理解

防抖就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

举例说明

坐电梯的时候,如果电梯检测到有人进来(触发事件),就会多等待 10 秒,此时如果又有人进来(10秒之内重复触发事件),那么电梯就会再多等待 10 秒。

在上述例子中,电梯在检测到有人进入 10 秒钟之后,才会关闭电梯门开始运行,因此,“函数防抖”的关键在于,在 一个事件 发生 一定时间 之后,才执行 特定动作

应用场景

在开发过程中遇到的高频率触发事件

mousedown、mousemove、keyup、keydown,resize
input搜索事件,表单提交多次频繁点击

实现方式

第一版(demo1代码)
import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 1000);

  render() {
    return <button onClick={this.handleAddCount}>{this.state.count}</button>;
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;

  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func;
    }, wait);
  };
}
  • 问:这个函数用到的主要知识点是什么,这个函数可以使用吗?

    举例说明:

import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 1000);

  render() {
    return <button onClick={this.handleAddCount}>{this.state.count}</button>;
  }
}

结果:闭包,用不了

原因:this 指向问题

我们可知在setTimeout 中this 指向windows 详情可 MDN看详细说明

JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:

 function setTimeout(fn,delay) { 
 // 等待 delay 毫秒 
 fn(); // <-- 调用位置! 
 } 
第二版(修改this指向问题- demo2代码)
import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo2 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 5000);

  render() {
    return <button onClick={this.handleAddCount}>{this.state.count}</button>;
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;
  return function() {
    const this_ = this;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this_);
    }, wait);
  };
}
  • 问: 修改好this指向以后,这个函数可以用吗?

结果:可以使用了,但是还有问题

大家看看这个函数还有什么问题?

修改上面部分代码:(demo3)

import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo3 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce((params: number) => {
    console.log(params, 'params');
    this.setState({ count: this.state.count + params }, () => {
      console.log(this.state.count, 'count');
    });
  }, 1000);

  render() {
    return (
      <button
        onClick={() => {
          this.handleAddCount(20);
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;

  return () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this);
    }, wait);
  };
}

结果:结果为NaN,我们可以知道,函数的参数丢失了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3B9b1NVk-1629202244062)(C:\Users\wangj\AppData\Local\Temp\1628754204830.png)]

第三版(修改参数丢失问题-demo4)
import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo4 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce((params: number) => {
    this.setState({ count: this.state.count + params }, () => {
      console.log(this.state.count, 'count');
    });
  }, 1000);

  render() {
    return (
      <button
        onClick={() => {
          this.handleAddCount(20);
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;
  return (...args: any) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

问:修改号参数丢失问题后,该函数还有什么问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nl6EPABz-1629202244064)(C:\Users\wangj\AppData\Local\Temp\1628754642565.png)]

举例说明(demo5)

import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo5 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce((params: number) => {
    this.setState({ count: this.state.count + params }, () => {
      console.log(this.state.count, 'count');
    });
    return this.state.count;
  }, 1000);

  handleChange = () => {
    this.handleAddCount(1);
    console.log(this.handleAddCount(1), 'this.handleAddCount(1);');
  };

  render() {
    return (
      <button
        onClick={() => {
          this.handleChange();
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;
  return (...args: any) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

通过以上代码我们发现,当前代码无法获取到函数的返回值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WX3VkRtD-1629202244065)(C:\Users\wangj\AppData\Local\Temp\1628760189072.png)]

答:由上方可以看出,函数返回值丢失了

第四版(函数添加返回值)

因为setTimeout 为异步函数的问题和this指向的问题,导致延时器中返回的函数只能是undefined,那么我们怎么来处理这个问题呢?

方法比较多,具体大家可以参考异步函数返回值

在这我主要写两种最常见的方法:回调函数,promise

回调函数(demo6)

import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  getCount = (params: any) => {
    console.log(params, 'params');
  };

  handleAddCount = debounce(
    (params: number) => {
      this.setState({ count: this.state.count + params }, () => {
        console.log(this.state.count, 'count');
      });
      return this.state.count;
    },
    1000,
    this.getCount,
  );

  handleChange = () => {
    this.handleAddCount(1);
  };
  render() {
    return (
      <button
        onClick={() => {
          this.handleChange();
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

function debounce(func: any, wait: number, callback: any): any {
  let timer: NodeJS.Timeout;
  return (...args: any) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      callback(func.apply(this, args));
    }, wait);
  };
}

promise (demo7)

import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce((params: number) => {
    this.setState({ count: this.state.count + params }, () => {
      console.log(this.state.count, 'count');
    });
    return this.state.count;
  }, 8000);

  handleChange = () => {
    this.handleAddCount(1).then(params => {
      console.log(params, 'params');
    });
  };
  render() {
    return (
      <button
        onClick={() => {
          this.handleChange();
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

function debounce(func: any, wait: number): any {
  let timer: NodeJS.Timeout;
  return (...args) => {
    return new Promise(function(resolve) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        resolve(func.apply(this, args));
      }, wait);
    });
  };
}

问:大家觉这一版函数有什么问题?

答:不符合防抖正常情况,不能立即执行

第五版(立即执行问题-demo8)
import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  getCount = (params: any) => {
    console.log(params, 'params');
  };

  handleAddCount = debounce(
    (params: number) => {
      this.setState({ count: this.state.count + params }, () => {
        console.log(this.state.count, 'count');
      });
      return this.state.count;
    },
    5000,
    true,
    this.getCount,
  );

  render() {
    return (
      <button
        onClick={() => {
          this.handleAddCount(1);
        }}
      >
        {this.state.count}
      </button>
    );
  }
}

// 第五版
function debounce(
  func: any,
  wait: number,
  immediate: boolean,
  callback: any,
): any {
  let timer: NodeJS.Timeout | null;
  return (...args: any) => {
    if (timer) clearTimeout(timer);
    if (immediate) {
      let runNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (runNow) {
        callback(func.apply(this, args));
      }
    } else {
      timer = setTimeout(() => {
        callback(func.apply(this, args));
      }, wait);
    }
  };
}
第六版(添加防抖取消代码-demo9)
import React, { Component } from 'react';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo9 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = debounce(
    (params: number) => {
      this.setState({ count: this.state.count + params }, () => {
        console.log(this.state.count, 'count');
      });
    },
    5000,
    true,
  );
  handleCancel = () => {
    this.handleAddCount.cancel();
  };

  render() {
    return (
      <div>
        <button
          onClick={() => {
            this.handleAddCount(1);
          }}
        >
          {this.state.count}
        </button>
        <button
          onClick={() => {
            this.handleCancel();
          }}
        >
          取消防抖
        </button>
      </div>
    );
  }
}

function debounce(
  func: any,
  wait: number,
  immediate: boolean,
  callback?: any,
): any {
  let timer: NodeJS.Timeout | null;
  const debounced = (...args: any[]) => {
    const runFunction = () => {
      return callback
        ? callback(func.apply(this, args))
        : func.apply(this, args);
    };

    if (timer) clearTimeout(timer);
    if (immediate) {
      let runNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (runNow) {
        runFunction();
      }
    } else {
      timer = setTimeout(() => {
        runFunction();
      }, wait);
    }
  };

  debounced.cancel = function() {
    timer && clearTimeout(timer);
    timer = null;
  };

  return debounced;
}
第七版(react hook 防抖实现-demo10)

我们将第六版的函数直接用到react hook 版本中

import React, { useState, useRef, useEffect } from 'react';

const demo10: React.FC<any> = () => {
  const [count, setCount] = useState<number>(0);

  const handleAddCount = UseDebounce(
    () => {
      setCount(count => count + 1);
    },
    3000,
    false,
  );

  return (
    <div>
      <button onClick={handleAddCount}>count|{count}</button>
    </div>
  );
};
export default demo10;

// 普通函数
function UseDebounce(
  func: any,
  wait: number,
  immediate: boolean,
  callback?: any,
): any {
  let timer: NodeJS.Timeout | null;
  const debounced = (...args: any[]) => {
    const runFunction = () => {
      return callback
        ? callback(func.apply(this, args))
        : func.apply(this, args);
    };

    if (timer) clearTimeout(timer);
    if (immediate) {
      let runNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (runNow) {
        runFunction();
      }
    } else {
      timer = setTimeout(() => {
        runFunction();
      }, wait);
    }
  };

  debounced.cancel = function() {
    timer && clearTimeout(timer);
    timer = null;
  };

  return debounced;
}

发现函数失效了,

注意点:在上面例子中,debounce的第三个函数为false 时,闭包的原因导致,导致定时器timer 未被清理, 从而导致防抖起作用

失效原因分析:

在执行setCount 时,count 改变导致函数重建,延时器timer 被清除,在这我们只需要保证timer不被清除就可以了,我们使用useRef来缓存变量timer

function UseDebounce(
  func: any,
  wait: number,
  immediate: boolean,
  callback?: any,
): any {
  let timer = useRef<NodeJS.Timeout | null>();
  const debounced = (...args: any[]) => {
    console.log('渲染');

    const runFunction = function() {
      console.log('ddd');

      return callback
        ? callback(func.apply(this, args))
        : func.apply(this, args);
    };

    if (timer.current) clearTimeout(timer.current);

    if (immediate) {
      let runNow = !timer.current;
      timer.current = setTimeout(() => {
        timer.current = null;
      }, wait);
      if (runNow) {
        runFunction();
      }
    } else {
      timer.current = setTimeout(() => {
        runFunction();
      }, wait);
    }
  };

  debounced.cancel = function() {
    timer.current && clearTimeout(timer.current);
    timer.current = null;
  };

  return debounced;
}

第八版(react hook 优化-demo11)

在demo10中存在UseDebounce 多次生成的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PdYDGpA1-1629202244067)(C:\Users\wangj\AppData\Local\Temp\1629103502231.png)]

我们怎么解决这种情况呢?

  1. 借助 useCallback 函数的第二个参数,做依赖更新。

  2. 借助 useRef 永久存贮,借助 useEffect 更新永久存贮。

    import React, { useState, useCallback, useEffect, useRef } from 'react';
    
    const demo11: React.FC<any> = () => {
      const [count, setCount] = useState<number>(0);
    
      const handleClick = useCallback(data => {
        console.log(data, 'data');
      }, []);
    
      const func = React.useCallback(() => {
        setCount(count => count + 1);
        return 2;
      }, []);
    
      const handleAddCount = UseDebounce(func, 3000, true, handleClick);
    
      return (
        <div>
          <button onClick={handleAddCount}>count|{count}</button>
        </div>
      );
    };
    export default demo11;
    
    function UseDebounce(
      func: any,
      wait: number,
      immediate: boolean,
      callback?: any,
    ): any {
      let timer = useRef<NodeJS.Timeout | null>();
      const fnRef = useRef<any>(func);
    
      useEffect(() => {
        fnRef.current = func;
      }, [func]);
    
      const timerCancel = function() {
        if (timer.current) clearTimeout(timer.current);
      };
      console.log('渲染');
    
      function debounced(...args: any[]) {
        const runFunction = () => {
          return callback
            ? callback(fnRef.current.apply(fnRef.current, args))
            : fnRef.current.apply(fnRef.current, args);
        };
    
        timerCancel();
        if (immediate) {
          let runNow = !timer.current;
          timer.current = setTimeout(() => {
            timer.current = null;
          }, wait);
          if (runNow) {
            runFunction();
          }
        } else {
          timer.current = setTimeout(() => {
            runFunction();
          }, wait);
        }
      }
    
      debounced.cancel = function() {
        timerCancel();
        timer.current = null;
      };
    
      return useCallback(debounced, [wait, immediate, timerCancel, func]);
    }
    
    

节流

概念理解

节流就是指连续触发事件在 n 秒中只执行一次函数。节流会稀释函数的执行频率

举例说明

坐火车或地铁,过安检的时候,在一定时间(例如10秒)内,只允许一个乘客通过安检入口,以配合安检人员完成安检工作。上例中,每10秒内,仅允许一位乘客通过。

分析可知,“函数节流”的要点在于,在 一定时间 之内,限制 一个动作 只 执行一次 。

应用场景

  1. scroll 事件,每隔一秒计算一次位置信息等
  2. 浏览器播放事件,每个一秒计算一次进度信息等
  3. input 框实时搜索并发送请求展示下拉列表,没隔一秒发送一次请求 (也可做防抖)

实现方式

第一版(时间戳版-demo1)
import React, { Component } from 'react';
import '../index.less';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = throttle(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 3000);

  render() {
    return (
      <div className="box" onMouseOver={this.handleAddCount}>
        {this.state.count}
      </div>
    );
  }
}

function throttle(func: any, wait: number) {
  let previous: number = 0;
  return (...args: any) => {
    let now = new Date().getTime();
    if (now - previous > wait) {
      func.apply(this, args);
      previous = now;
    }
  };
}

第二版(定时器版-demo2)
import React, { Component } from 'react';
import '../index.less';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = throttle(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 3000);

  render() {
    return (
      <div className="box" onMouseOver={this.handleAddCount}>
        {this.state.count}
      </div>
    );
  }
}

function throttle(func: any, wait: number) {
  let timer: NodeJS.Timeout | null = null;
  return (...args: any) => {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        func.apply(this, args);
      }, wait);
    }
  };
}

现在我们分析一下,上面两版

第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

所以我们将这两版总结一下,实现一版,鼠标移入能立即执行,但是停止触发的时候还能再次执行一次,代码如下:

第三版(二合一版-demo3)
import React, { Component } from 'react';
import '../index.less';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleAddCount = throttle(() => {
    this.setState({ count: this.state.count + 1 }, () => {
      console.log(this.state.count, 'count');
    });
  }, 3000);

  render() {
    return (
      <div className="box" onMouseOver={this.handleAddCount}>
        {this.state.count}
      </div>
    );
  }
}

function throttle(func: any, wait: number) {
  var previous = 0;
  let timer: NodeJS.Timeout | null;

  const runFunction = function(...args: any[]) {
    previous = new Date().getTime();
    timer = null;
    func.apply(this, args);
  };

  const throttled = (...args: any[]) => {
    var now = new Date().getTime();
    var remaining = wait - (now - previous);
    if (remaining <= 0 || remaining > wait) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      func.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(runFunction, remaining);
    }
  };
  return throttled;
}

上面的代码还有许多局限,比如说,我想要实现,鼠标移入不执行,但是停止触发的时候再执行一次的效果,或者鼠标移入立即执行,但是停止触发的时候不再执行的效果。对于这种情况我们应该如何去做呢?

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

同时在加上函数的回调函数,和节流取消

我们来优化一下代码:

第四版(demo4代码)
import React, { Component } from 'react';
import '../index.less';

type TypeProps = any;
type TypeState = {
  count: number;
};

export default class demo1 extends Component<TypeProps, TypeState> {
  constructor(props: Readonly<TypeProps>) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  getCount = (data: any) => {
    console.log(data, 'data');
  };

  handleAddCount = throttle(
    () => {
      this.setState({ count: this.state.count + 1 }, () => {
        console.log(this.state.count, 'count');
      });
      return 2;
    },
    10000,
    {
      trailing: false,
    },
    this.getCount,
  );

  handleCancel = () => {
    this.handleAddCount.cancel();
  };

  render() {
    return (
      <div>
        <div className="box" onMouseOver={this.handleAddCount}>
          {this.state.count}
        </div>
        <button
          onClick={() => {
            this.handleCancel();
          }}
        >
          取消节流
        </button>
      </div>
    );
  }
}

function throttle(func: any, wait: number, options?: any, callback?: any) {
  let previous: number = 0;
  let timer: NodeJS.Timeout | null = null;
  if (!options) {
    options = {};
  }

  const timerCancel = function() {
    if (timer) clearTimeout(timer);
  };

  const runFunction = (...args: any) => {
    previous = options.leading === false ? 0 : new Date().getTime();
    timer = null;
    callback ? callback(func.apply(this, args)) : func.apply(this, args);
    if (!timer) args = null;
  };

  const throttled = (...args: any) => {
    let now = new Date().getTime();
    if (!previous && options.leading === false) previous = now;
    let remaining = wait - (now - previous);
    if (remaining <= 0 || remaining > wait) {
      timerCancel();
      previous = now;
      callback ? callback(func.apply(this, args)) : func.apply(this, args);
      if (!timer) args = null;
    } else if (!timer && options.trailing !== false) {
      timer = setTimeout(runFunction, remaining);
    }
  };

  throttled.cancel = function() {
    timerCancel();
    timer = null;
    previous = 0;
  };

  return throttled;
}

我们要注意有这样一个问题:

那就是 leading:false 和 trailing: false 不能同时设置。

如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});
第五版(react hook 节流实现-demo5)
import React, { useState, useRef, useEffect, useCallback } from 'react';

const demo5: React.FC<any> = () => {
  const [count, setCount] = useState<number>(0);

  const handleClick = useCallback(data => {
    console.log(data, 'data');
  }, []);

  const handleAddCount = UseThrottle(
    () => {
      setCount(count => count + 1);
      return 2;
    },
    3000,
    true,
    handleClick,
  );

  const handleCancel = useCallback(() => {
    handleAddCount.cancel();
  }, []);

  return (
    <div>
      <div className="box" onMouseOver={handleAddCount}>
        {count}
      </div>
      <button onClick={handleCancel}>取消节流</button>
    </div>
  );
};
export default demo5;

function UseThrottle(func: any, wait: number, options?: any, callback?: any) {
  const previous = useRef<number>(0);
  const timer = useRef<NodeJS.Timeout | null>();
  const fnRef = useRef<any>(func);
  if (!options) {
    options = {};
  }

  useEffect(() => {
    fnRef.current = func;
  }, []);

  const runFunction = (...args: any) => {
    previous.current = options.leading === false ? 0 : new Date().getTime();
    timer.current = null;
    callback
      ? callback(func.apply(fnRef.current, args))
      : func.apply(fnRef.current, args);
    if (!timer) args = null;
  };

  const timerCancel = function() {
    if (timer.current) clearTimeout(timer.current);
  };

  const throttled = (...args: any) => {
    let now = new Date().getTime();
    if (!previous.current && options.leading === false) previous.current = now;
    let remaining = wait - (now - previous.current);
    if (remaining <= 0 || remaining > wait) {
      if (timer.current) {
        clearTimeout(timer.current);
        timer.current = null;
      }
      previous.current = now;
      callback
        ? callback(func.apply(fnRef.current, args))
        : func.apply(fnRef.current, args);
      if (!timer.current) args = null;
    } else if (!timer.current && options.trailing !== false) {
      timer.current = setTimeout(runFunction, remaining);
    }
  };

  throttled.cancel = function() {
    timerCancel();
    timer.current = null;
    previous.current = 0;
  };

  return useCallback(throttled, [wait, options, timerCancel, func]);
}

注意点

当我在实现react中触发input 组件中的onChange 事件时去发送一个异步请求,但是发现一直获取不到value 值,代码如下

  const onSearchChange = (e) => {
    e.persist()
    e.preventDefault()
    e.stopPropagation()
    setSearchValue(e.currentTarget.value)
    
    useDebounce(() => {
      setFilterValue(e.currentTarget.value)
    }, 500)
  }

就是setFilterValue 一直获取不到值,最后经过调查发现了原由,
首先,在 onSearchChange 中,事件触发,就能获取到 event 对象,其中主要就是 event.target 就是当前触发事件的 dom 对象,由于 useDebounce延迟执行,导致了 onSearchChange 函数已经执行完了,进入了 react-dom 中相关一系列操作(进行了一系列复杂的操作),下面给出最关键的 executeDispatchesAndRelease,executeDispatchesAndRelease 方法释放 event.target 的值

        /**
 * Dispatches an event and releases it back into the pool, unless persistent.
 *
 * @param {?object} event Synthetic event to be dispatched.
 * @private
 */
        var executeDispatchesAndRelease = function(event) {
            if (event) {
                executeDispatchesInOrder(event);
                if (!event.isPersistent()) {
                    event.constructor.release(event);
                }
            }
        };

由于 event 在 useDebounce中作为了参数,内存中没有清除,执行上面的方法 event.target = null; event 为引用类型,一处改变,所有用到的地方都会改变。导致了 useDebounce中 event 也发生了变化。

解决办法:

  const onSearchChange = (e) => {
    e.persist()
    e.preventDefault()
    e.stopPropagation()
    setSearchValue(e.currentTarget.value)
    handleSearchChange(e.currentTarget.value)
  }

  const handleSearchChange = useDebounce((value) => {
    setFilterValue(value)
  }, 500)

参考文档:
JavaScript专题之跟着 underscore 学节流
React hooks 怎样做防抖?

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值