Open wuxianqiang opened 4 years ago
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); // 这个 effect 依赖于 `count` state }, 1000); return () => clearInterval(id); }, []); // Bug: `count` 没有被指定为依赖 return <h1>{count}</h1>; }
传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此,count 永远不会超过 1。
[]
setInterval
count
setCount(0 + 1)
指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:
[count]
setTimeout
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量 }, 1000); return () => clearInterval(id); }, []); // 我们的 effect 不适用组件作用域中的任何变量 return <h1>{count}</h1>; }
(setCount 函数的身份是被确保稳定的,所以可以放心的省略掉)
setCount
此时,setInterval 的回调依旧每秒调用一次,但每次 setCount 内部的回调取到的 count 是最新值(在回调中变量命名为 c)。
c
在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。 useReducer 的 dispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。
const initialState = {count: 0} function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; default: throw new Error() } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { const id = setInterval(() => { dispatch({type: 'increment'}) }, 1000); return () => setInterval(id); }, []); return <h1>{state.count}</h1>; }
(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
useReducer
useState
function Child (props) { console.log('重新渲染') function handleClick () { props.dispatch({type: 'increment'}) } return ( <button onClick={handleClick}>按钮</button> ) } Child = React.memo(Child) function Counter() { const [state, dispatch] = useReducer(reducer, initialState); function handleClick() { dispatch({type: 'increment'}) } return ( <div> <h1 onClick={handleClick}>{state.count}</h1> <Child dispatch={dispatch} /> </div> ) }
React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
function Counter() { const [state, dispatch] = useReducer(reducer, initialState); function handleClick() { dispatch({type: 'increment'}) } const callback = useCallback(() => { dispatch({type: 'increment'}) }, []) return ( <div> <h1 onClick={handleClick}>{state.count}</h1> <Child callback={callback} /> </div> ) }
如果直接从useReducer返回操作,则其行为与useState几乎相同。
function App() { const [name, setName] = useReducer((_, value) => value, '请输入'); return ( <div className="App"> <input value={name} onChange={e => setName(e.target.value)} /> </div> ); }
有时候,你的 effect 可能会使用一些频繁变化的值。你可能会忽略依赖列表中 state,但这通常会引起 Bug:
传入空的依赖数组
[]
,意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在setInterval
的回调中,count
的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将count
的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行setCount(0 + 1)
,因此,count
永远不会超过 1。指定
[count]
作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个setInterval
在被清除前(类似于setTimeout
)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定 state 该 如何 改变而不用引用 当前 state:(
setCount
函数的身份是被确保稳定的,所以可以放心的省略掉)此时,
setInterval
的回调依旧每秒调用一次,但每次setCount
内部的回调取到的count
是最新值(在回调中变量命名为c
)。在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外。 useReducer 的 dispatch 的身份永远是稳定的 —— 即使 reducer 函数是定义在组件内部并且依赖 props。
(如果你熟悉 Redux 的话,就已经知道它如何工作了。)
在某些场景下,
useReducer
会比useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
如果直接从useReducer返回操作,则其行为与useState几乎相同。