4

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

 1 year ago
source link: https://www.daozhao.com/10888.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

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

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

function debounce(handler, wait) {
  let timeId = null;
  return function(...rest) {
    timeId && clearTimeout(timeId);
    timeId = setTimeout(function () {
      handler.apply(this, rest)
    }, wait);
  }
}
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组件外,比如把代码调整成这样。

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很多情况用不上。

我们继续回到代码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大法了

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

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

现在一切都好了,但是并不是终点,我们是不是可以换个方式思考下:既然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即可。 我们再也不用为此改业务代码了,闭包陷阱什么的都不再是问题,是不是很爽?


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK