useEffect
// deps 是依赖数组,可以为空
useEffect(() => {
console.log('useEffect')
}, [deps]);
useEffect的使用 参考上篇文章
执行时机:
- useEffect可以看做
componentDidMount、componentDidUpdate、componentWillUnmount
生命周期的结合。 - 只有当
deps
里的变量发生改变时,才会执行。当第二参数是空数组,该effect 只会在组件didMount和willUnmount后执行;当第二个参数为空时,组件每次渲染都会执行。
useMemo
// 返回一个有记忆的值(memoized)
const memoizedValue = useMemo(() => computeExpensiveValue(deps),deps);
// useMemo 可以帮助避免子组件的不必要的rerender
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<div>
{child1}
{child2}
</div>
)
}
执行时机:
-
useMemo
在 组件rendering 的时候执行,在useEffect之前。因此副作用应放在useEffect里,而不是useMemo。 - 只有当
deps
发生变化时,useMemo
才会重新计算memoizedValue
, 避免渲染时不必要的计算。当第二个参数为空时,组件每次渲染都会执行。
useCallback
// 返回一个有记忆的callback
const memoizedCallback = useCallback(
() => {
doSomething(deps);
},
[deps],
);
执行时机:
- 只有当deps发生变化时,memoizedCallback才会改变。这对通过传递callbacks来优化子组件的渲染很有帮助。(配合子组件的shouldComponentUpdate 或者 React.memo 起到减少不必要的渲染的作用)
// useCallback 与useMemo
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
useCallback 的真正目的还是在于缓存了每次渲染时 inline callback 的实例,这样方便配合子组件的 shouldComponentUpdate 或者 React.memo 起到减少不必要的渲染的作用。
useCallback 适用于所有的场景吗?
我们知道useCallback
也是有依赖项的,如果一个 callback
依赖于一个经常变化的 state
,这个 callback
的引用是无法缓存的。React 文档的 FAQ 里也提到了这个问题,还原一下问题的场景:
function Form() {
const [text, setText] = useState('');
const handleSubmit = useCallback(() => {
console.log(text);
}, [text]); // 每次 text 变化时 handleSubmit 都会变
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} /> // 很重的组件
</div>
);
}
官网提出,如果要记忆化的callback函数是一个事件处理器,rendering时不需要调用,那么,你可以用useRef
创建一个实例变量,手动把最新的值存进来。例如:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // Write it to the ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // Read it from the ref
alert(currentText);
}, [textRef]); // Don't recreate handleSubmit like [text] would do
return (
<div>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</div>
);
}
但是,上面的额解决方案也有缺陷,只有在 DOM 更新时才对 ref.current 做更新,会导致在 render 阶段不能调用这个函数。更严重的是,因为对 ref 做了修改,在未来的 React 异步模式下可能会有诡异的情况出现(因此上文中官方的解法也是”异步模式不安全“的)。
怎么避免深度传递callback?
考虑到上面提到的弊端,目前推荐的解决方案是使用useReducer,传递dispatch通过context。reducer 其实是在下次 render 时才执行的,所以在 reducer 里,访问到的永远是新的 props 和 state。
const TodosDispatch = React.createContext(null);
function TodosApp() {
// useReducer 返回的 dispatch 函数是自带 memoize 的
// Note: `dispatch` won't change between re-renders
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
Any child in the tree inside TodosApp can use the dispatch function to pass actions up to TodosApp:
function DeepChild(props) {
// If we want to perform an action, we can get dispatch from context.
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
TodosApp的任何一个子组件都可以使用dispatch函数向TodosApp传递actions.
如果你想同时把 state 作为 context 传递下去,请分成两个 context 来声明。