道招

React函数函数式组件的防抖失效和闭包陷阱只会二选一?

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

React函数函数式组件的防抖失效和闭包陷阱只会二选一?

项目中输入搜索联想的场景我们通常会加入防抖,减少对服务端造成的压力,在React的函数式组件中使用的时候一不小心就掉进坑里了。 我们的防抖函数实现如下

function debounce(handler, wait) {
  let timeId = null;
  return function(...rest) {
    timeId && clearTimeout(timeId);
    timeId = setTimeout(function () {
      handler.apply(this, rest)
    }, wait);
  }
}

代码1

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Input } from 'antd';

function debounce(handler, wait) {
  let timeId = null;
  return function(...rest) {
      timeId && clearTimeout(timeId);
      timeId = setTimeout(function () {
        handler.apply(this, rest)
      }, wait);
    }
}

let currentValue;

function SearchInput(props) {
  const [value, setValue] = useState();
  const fnRef = useRef();

  async function rawFetch(value, callback) {
    console.log('====value====', value);
    currentValue = value;

    fetch(`https://www.baidu.com`).then(res => {}).catch(err => {
      callback('error: ', err.message)
    })
  }

  // 原始
  const debouncedFetch = debounce(rawFetch, 300);

  const handleChange = e => {
    const value = e.target.value;
    setValue(value);
    debouncedFetch(value, console.log);
  };

  return (
    <div>
      <input
        value={value}
        onChange={handleChange}
      />
    </div>
  );
}

export default SearchInput;

file

实测的时候发现防抖并没有发挥作用,还是每次在调用接口,这是怎么回事呢

这是因为每次输入触发rerender的时候,都会重新生成一个debounce的fetch,虽然各个fetch是防抖的,生成的这些fetch当timeout之后都会触发请求。

怎么解决呢?我们应该阻止每次都生成一个新的,很自然的想法就是把fetch相关的代码挪到SearchInput组件外,比如把代码调整成这样。

代码2

import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { Input } from 'antd';

function debounce(handler, wait) {
  let timeId = null;
  return function(...rest) {
      timeId && clearTimeout(timeId);
      timeId = setTimeout(function () {
        handler.apply(this, rest)
      }, wait);
    }
}

async function rawFetch(value, callback) {
    console.log('====value====', value);
    currentValue = value;

    fetch(`https://www.baidu.com`).catch(() => {}).then(() => {
        callback('111')
    })
}

const debouncedFetch = debounce(rawFetch, 300);

let currentValue;

function SearchInput(props) {
  const [value, setValue] = useState();
  const fnRef = useRef();

  const handleChange = e => {
    const value = e.target.value;
    setValue(value);
    debouncedFetch(value, console.log);
  };

  return (
    <div>
      <input
        value={value}
        onChange={handleChange}
      />
    </div>
  );
}

export default SearchInput;

这样是有效果,但是在示例中rawFetch的回调里面只是简单写了个console.log,实际的场景可能比这个复杂多了,所以此方案代码2很多情况用不上。

代码3

我们继续回到代码1,其实我们是可以用useCallback来确保rerender之后debouncedFetch仍然是用的同一个 我们只用把之前的上面的debouncedFetch替换一下即可

const debouncedFetch = useCallback(debounce(rawFetch, 300), []);

现在是没问题了,但是依然存在有缺陷,因为更多的时候我们的rawFetch里面valuecallback都是外部调用时传入的。更多的时候需要直接去当前作用域下的取值,或者说它里面还嵌套有其它方法,其它方法也在当前作用域取值。 比如上述的rawFetch改成这样,value不在从参数中获取,而是直接从当前作用域里面获取

async function rawFetch(_, callback) {
    console.log('====value====', value);
    currentValue = value;

    fetch(`https://www.baidu.com`).then(res => {}).catch(err => {
      callback('error: ', err.message)
    })
}

此时防抖依然有效,但是rawFetch里面取value就一直是undefined了,掉入闭包陷阱了。

file

怎么办? 我们只能用useRef大法了

代码4

我们继续基于代码1调整debouncedFetch

const fnRef = useRef();
fnRef.current = rawFetch;
const debouncedFetch = useCallback(debounce(() => fnRef.current(), 300), []);

代码5

现在一切都好了,但是并不是终点,我们是不是可以换个方式思考下:既然React函数式组件既然容易有闭包陷阱,为什么我们的防抖一定要在React组件这一层呢,直接在原始的接口调用fetch上做防抖不香吗?

说干就干

我们把自己的防抖函数改造下,让它返回promise,和我们平时用的fetch或者axios保持一致。

function debouncePromise(handler, wait) {
  let timeId = null;
  return function(...rest) {
    return new Promise((resolve) => {
      timeId && clearTimeout(timeId);
      timeId = setTimeout(function () {
        resolve(handler.apply(this, rest));
      }, wait);
    }).catch(err => {
      console.log('err ~ ', err);
    })
  }
}

然后的上面的const debouncedFetch = debounce(rawFetch, 300);也可以不用了

只是使用const debouncedFetch = rawFetch;即可。在rawFetch里面直接使用我们防抖后的myFetch; 很显然,rawFetch可以跟代码2中一样,直接拿到React之外。

const myFetch = debouncePromise(fetch, 300);

完整代码如下

function debouncePromise(handler, wait) {
  let timeId = null;
  return function(...rest) {
    return new Promise((resolve) => {
      timeId && clearTimeout(timeId);
      timeId = setTimeout(function () {
        resolve(handler.apply(this, rest));
      }, wait);
    }).catch(err => {
      console.log('err ~ ', err);
    })
  }
}

const myFetch = debouncePromise(fetch, 300);

let currentValue;
function SearchInput(props) {
  const [value, setValue] = useState();
  const fnRef = useRef();

  async function rawFetch(_, callback) {
    console.log('====value====', value);
    currentValue = value;

    myFetch(`https://www.baidu.com`).then(res => {}).catch(err => {
      callback('error: ', err.message)
    })
  }

  const handleChange = e => {
    const value = e.target.value;
    setValue(value);
    rawFetch(value, console.log);
  };

  return (
    <div>
      <input
        value={value}
        onChange={handleChange}
      />
    </div>
  );
}

export default SearchInput;

后续我们所有的这类场景,只用改下公共的接口调用处代码,判断下是要用原始的fetch还是debouncePromise防抖后的fetch即可。 我们再也不用为此改业务代码了,闭包陷阱什么的都不再是问题,是不是很爽?

更新时间:
上一篇:微前端qiankun问题You need to export the functional lifecycles in xxx entry终极解决方案下一篇:文本划词标注功能代码实现

相关文章

vue里面使用debounce,throttle注意点

我们有时在自己的vue项目中不可避免的要监听类似scroll事件的,这是如果相对性能影响少点,只能会想到debounce防抖之类的。但是我们要注意了,addEventListener和removeE 阅读更多…

关注道招网公众帐号
友情链接
消息推送
道招网关注互联网,分享IT资讯,前沿科技、编程技术,是否允许文章更新后推送通知消息。
允许
不用了