这是我参与更文挑战的第 9 天,活动详情查看:更文挑战
什么是闭包陷阱
const FunctionComponent = () => {
const [value, setValue] = useState(1)
const log = () => {
setTimeout(() => {
alert(value)
}, 1000);
}
return (
<div>
<p>FunctionComponent</p>
<div>value: {value}</div>
<button onClick={() => setValue(value + 1)}>add</button>
<br/>
<button onClick={log}>alert</button>
</div>
)
}
复制代码
在上面的函数式组件中,我们点击 alert 按钮后会在 1s 后弹出 value 的值,我们在这 1s 的时间内可以继续点击 add 按钮增加 value 的值。
上图是我们操作的结果。我们发现弹出的值和当前页面显示的值不相同。换句话说:log 方法内的 value 和点击动作触发那一刻的 value 相同,value 的后续变化不会对 log 方法内的 value 造成影响。这种现象被称为“闭包陷阱”或者被叫做“Capture Value” :函数式组件每次render 都会生产一个新的 log 函数,这个新的 log 函数会产生一个在当前这个阶段 value 值的闭包。
上面例子 “闭包陷阱” 的分析:
- 初始次渲染,生成一个 log 函数(value = 1)
- value 为 1 时,点击 alert 按钮执行 log 函数(value = 1)
- 点击按钮增加 value,比如 value 增加到 6,组件 render ,生成一个新的 log 函数(value = 6)
- 计时器触发,log 函数(value = 1)弹出闭包内的 value 为 1
解决闭包陷阱的方案
使用 useRef 解决闭包陷阱的问题
const FunctionComponent = () => {
const [value, setValue] = useState(1)
const countRef = useRef(value)
useEffect(() => {
countRef.current = value
}, [value])
const log = useCallback(
() => {
setTimeout(() => {
alert(countRef.current)
}, 1000);
},
[value],
)
return (
<div>
<p>FunctionComponent</p>
<div>value: {value}</div>
<button onClick={() => setValue(value + 1)}>add</button>
<br/>
<button onClick={log}>alert</button>
</div>
)
}
复制代码
useRef 每次 render 时都会返回同一个引用类型的对象,我们设置值和读取值都在这个对象上处理,这样就能获取到最新的 value 值了。
更新 State 时的回调函数
Effect Hook 可以让你在函数组件中执行副作用操作
假设现在我们要开一个每秒自增的计数器,我们一般会写出下面这样的代码:
const Counter = () => {
const [value, setValue] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log('new value:', value+1)
setValue(value + 1)
}, 1000);
return () => {
clearInterval(timer)
}
}, [])
return (
<div>
<p>Counter</p>
<div>count: {value}</div>
</div>
)
}
复制代码
上面的代码中,我们在 useEffect
中不断更新 value 的值,但是结合我们之前的闭包陷阱问题来分析,我们可以发现定时器的value值永远都会是 0,这就导致每次设置的 value 值都是 1,下图是运行的结果。
“闭包陷阱” 最大的问题就是在函数数内无法获取的最新的 state 的值,那 React 提供了哪些方法来解决呢?
- useRef 上面已有介绍
- useState 更新值时传入回调函数
除了上面介绍的 useRef 的方法外,我们也可以在更新 state 时我们传入回调函数(回调函数里取到的值是最新的)。
const [value, setValue] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
// 回调函数的最新值
setValue(value => value + 1)
}, 1000);
return () => {
clearInterval(timer)
}
}, [])
复制代码
闭包陷阱和 Hooks 依赖
useEffect、useLayoutEffect、useCallback、useMemo 的第二个参数为依赖数组,依·赖数组中任意一个依赖变化(浅比较)会有如下效果:
- useEffect、useLayoutEffect 内部的副作用函数会执行,并且副作用函数可以获取到当前所有依赖的最新值。
- useCallback、useMemo 会返回新的函数或对象,并缺内部的函数也能获取到当前所有依赖的最新值。
利用这个机制理论可以解决“闭包陷阱”,但是在某种情况下不适用:
const Counter = () => {
const [value, setValue] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log('tick:', value+1)
setValue(value + 1)
}, 1000);
return () => {
console.log('clear')
clearInterval(timer)
}
- }, [])
+ }, [value])
return (
<div>
<p>Counter</p>
<div>count: {value}</div>
</div>
)
}
复制代码
上面的代码我们把 value 作为依赖项加入到依赖数组,却是能够实现功能,但是每次都会经历 clearInterval -> setValue ->clearInterval
的循环。这就造成了不必要的性能消耗。还有一种极端的情况,如果我们没有返回取消定时器的函数,就会不断增加新的定时器。
使用 Hook 依赖的注意事项
事件订阅
现在我们有如下的场景
useEffect(() =>{
someThing.subscribe(() => {
// do something with value
})
}, [value])
复制代码
上面的代码中,value 变化会不断订阅新的事件。所以在 EffectHook 中我们记得返回取消副作用的函数
useEffect(() =>{
someThing.subscribe(() => {
// do something with value
})
return () => {
+ // 添加取消副作用的函数
}
}, [value])
复制代码
防抖节流
function BadDemo() {
const [count, setCount] = useState(1);
const [, setRerender] = useState(false);
const handleClick = debounce(() => {
setCount(c => ++c);
}, 1000);
useEffect(() => {
// 每500ms,组件重新render
setInterval(() => {
setRerender(r => !r);
}, 500);
}, []);
return <div onClick={handleClick}>{count}</div>;
}
作者:蚂蚁保险体验技术
链接:https://juejin.cn/post/6844904090032406536
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码
比如上面的代码,我们有一个需要防抖的 handleClick
函数,但是我们函数会不时地渲染,每次 render 都会生成一个新的函数,那么这个防抖的函数就失去了作用。
总结
最后总结一下:
- React Hooks 存在“闭包渲染”的问题,每次 render 都会闭包缓存当前render对应的 state
- 可以通过 useRef、state 更新时的回调函数来解决这个问题
- 使用 EffectHook 依赖时要注意取消副作用