本文源自道招网的 # 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;
实测的时候发现防抖并没有发挥作用,还是每次在调用接口,这是怎么回事呢
这是因为每次输入触发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
里面value
和callback
都是外部调用时传入的。更多的时候需要直接去当前作用域下的取值,或者说它里面还嵌套有其它方法,其它方法也在当前作用域取值。
比如上述的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
了,掉入闭包陷阱了。
怎么办?
我们只能用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即可。 我们再也不用为此改业务代码了,闭包陷阱什么的都不再是问题,是不是很爽?