什么是 debounce

debounce,中文翻译为防抖。与之相对应的还有一个概念 throttle —— 节流。两者用来控制某个函数在一定时间内执行多少次的技巧,相似却又不同。
关于两者的对比以及使用场景,可以阅读这篇文章来加深了解。本文主要讲述的,是如果在 react components中,使用 debounce 函数来防止回调事件的多次触发,从而提升代码效率。

应用

在一个 input 输入框中,给其绑定了一个 onChange 事件,每次输入后,都会触发一个回调函数。因为我们想要的只是用户输入的最后结果,所以除了用户输入完成后触发的回调函数,其他都是冗余的。因此,需要对绑定的回调函数做 debounce 处理。
关于 debounce 函数,有很多实现版本,这里选用了 loadsh.debounce。虽然函数实现原理很简单,但是用于生产环境的代码,还是建议使用成熟的第三方库。
我们的第一反应,一般都是直接将回调函数用 debounce 包裹起来达到想要的效果。

  1. import react, { Component } from 'react';
  2. import { debounce } from 'lodash';
  3. export default class Debounce extends Component {
  4. printChange(e) {
  5. console.log('value :: ', e.target.value);
  6. // call ajax
  7. }
  8. render() {
  9. return (
  10. <div>
  11. <input onChange={debounce(this.printChange, 500)} />
  12. </div>
  13. );
  14. }
  15. }

但是这么做之后,浏览器就会报异常: Uncaught TypeError: Cannot read property 'value' of null。为什么会这样?这里就涉及到了 react 事件系统 中的一个概念:合成事件。

合成事件(SyntheticEvent)

事件处理程序通过 合成事件(SyntheticEvent)的实例传递,SyntheticEvent 是浏览器原生事件跨浏览器的封装。SyntheticEvent 和浏览器原生事件一样有 stopPropagation()preventDefault() 接口,而且这些接口夸浏览器兼容。

事件池(Event Pooling)

SyntheticEvent 是池化的. 这意味着 SyntheticEvent 对象将会被重用,并且所有的属性都会在事件回调被调用后被 nullified。 这是因为性能的原因。 因此,你不能异步的访问事件。
通过了解事件系统,也就不难理解为什么会报错了。因为经过 debounce 包装后的回调函数,变成了一个异步事件,在池化后被 nullified 了。
那么怎样才能解决这个问题?
通过在回调事件顶部加上 e.persist() 就可以从池中移除合成事件,并允许对事件的引用保留。

  1. import react, { Component } from 'react';
  2. import { debounce } from 'lodash';
  3. export default class Debounce extends Component {
  4. printChange(e) {
  5. e.persist();
  6. debounce(() => {
  7. console.log('value :: ', e.target.value);
  8. // call ajax
  9. });
  10. }
  11. render() {
  12. return (
  13. <div>
  14. <input onChange={this.printChange} />
  15. </div>
  16. );
  17. }
  18. }

这样做之后报错问题虽然解决了,但是会发现 debounce 的函数并没有被执行。因此,还需要对回调函数进行优化。
把需要异步执行的回调函数抽离出来封装,并且在组件初始化话的时候就将其 debounce 化,就可以得到我们想要的效果。

  1. import react, { Component } from 'react';
  2. import { debounce } from 'lodash';
  3. export default class Debounce extends Component {
  4. construtor() {
  5. super();
  6. this.callAjax = debounce(this.callAjax, 300);
  7. }
  8. callAjax = (value) => {
  9. console.log('value :: ', value);
  10. // call ajax
  11. }
  12. printChange(e) {
  13. e.persist();
  14. this.callAjax(e.target.value);
  15. }
  16. render() {
  17. return (
  18. <div>
  19. <input onChange={this.printChange} />
  20. </div>
  21. );
  22. }
  23. }

throttle的使用方法和debounce的使用方法一样