useEffect、useMemo、useCallback对于回调,对于数据,对于函数

useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。

回顾

在介绍一下这两个hooks的作用之前,我们先来回顾一下react中的性能优化。在hooks诞生之前,如果组件包含内部state,我们都是基于class的形式来创建组件。当时我们也知道,react中,性能的优化点在于:

  1. 调用setState,就会触发组件的重新渲染,无论前后的state是否不同
  2. 父组件更新,子组件也会自动的更新

基于上面的两点,我们通常的解决方案是:使用immutable进行比较,在不相等的时候调用setState;在shouldComponentUpdate中判断前后的props和state,如果没有变化,则返回false来阻止更新。
在hooks出来之后,我们能够使用function的形式来创建包含内部state的组件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我们无法通过判断前后状态来决定是否更新。而且,在函数组件中,react不再区分mount和update两个状态,这意味着函数组件的每一次调用都会执行其内部的所有逻辑,那么会带来较大的性能损耗。因此useMemo 和useCallback就是解决性能问题的杀手锏。

对比

我们先简单的看一下useMemo和useCallback的调用签名:

function> > useEffect> (> effect> : > EffectCallback> , > deps> ?> : > DependencyList> ): void;> function> > useMemo> <> T> >(> factory> : () > =>> > T> , > deps> : > DependencyList> | > undefined> ): > T> ;> function> > useCallback> <> T> > extends> (…> args> : > any> []) > =>> > any> >(> callback> : > T> , > deps> : > DependencyList> ): > T> ; useCallback和useMemo的参数跟useEffect一致。
他们之间最大的区别是useEffect会用于处理副作用,而前两个hooks不能。

useMemo和useCallback都会在组件第一次渲染的时候执行。
之后会在其依赖的变量发生改变时再次执行;
并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。

useEffect

使用场景:useEffect副作用,使函数组件拥有了类似react的声明周期。useEffect会在组件每次render之后调用,useEffect有两个参数,第一个为执行函数,第二个为数组[]

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合

  1. import React, { useState, useEffect } from 'react';
  2. function Example() {
  3. const [count, setCount] = useState(0);
  4. const [dataSources, setDataSources] = useState([]);
  5. /*
  6. * 情况一:useEffect无第二个参数
  7. */
  8. //组件初始化和render后,都会执行该useEffect
  9. useEffect(() => {
  10. console.log("相当于生命周期:componentDidMount+componentDidUpdate")
  11. });
  12. /*
  13. * 情况二:useEffect有第二个参数
  14. */
  15. //第二个参数为空数组时:组件初始化才执行
  16. useEffect(() => {
  17. console.log("相当于生命周期:componentDidMount");
  18. }, []);
  19. //第二个参数为指定状态值时:组件初始化时和dataSources发生变化才执行
  20. useEffect(() => {
  21. console.log("相当于生命周期:componentDidMount")
  22. console.log("相当于依赖dataSources状态值的生命周期:componentDidUpdate")
  23. }, [dataSources]);
  24. //执行函数内return一个函数:初始化时执行函数体,组件卸载unmount时执行return后的函数
  25. useEffect(() => {
  26. console.log("相当于生命周期:componentDidMount")
  27. // 执行函数中直接使用return返回一个函数,这个函数会在组件unmount时执行。
  28. return () => {
  29. console.log('相当于声明周期:componentWillUnmount');
  30. }
  31. }, []);
  32. return (
  33. <div>
  34. <p>You clicked {count} times</p>
  35. <button onClick={() => setCount(count + 1)}>
  36. Click me
  37. </button>
  38. </div>
  39. );
  40. }
  41. export default Example;

官方提示:与 componentDidMount 或 componentDidUpdate 不同,useEffect是异步的,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。官方建议尽可能使用useEffect,effect 不需要同步地执行 在个别情况下(例如测量布局,页面状态值闪烁bug时),才使用useLayoutEffect代替useEffect, 形成同步,在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制

useMemo

当[]依赖为空数组时候,可以初始化数据。
当[count]不为空时,每次count改变都会重新计算。

  1. const initGoods = useMemo(
  2. () =>
  3. pageDetail.goods.map(item => {
  4. const { termList = [], isOpenTrainingCamp } = item;
  5. if (isOpenTrainingCamp && termList.length !== 0) {
  6. return {
  7. ...item,
  8. hasTerm: true,
  9. skuBizNo: termList[0].termId,
  10. termDate: termList[0].beginTime,
  11. };
  12. }
  13. return item;
  14. }),
  15. [],
  16. );
  17. const [goods, setGoods] = useState(initGoods);

useCallback

讲完了useMemo,接下来是useCallback。useCallback跟useMemo比较类似,但它返回的是缓存的函数。
const fnA = useCallback(fnB, [a])

  1. import React, { useState, useCallback } from 'react';
  2. const set = new Set();
  3. export default function Callback() {
  4. const [count, setCount] = useState(1);
  5. const [val, setVal] = useState('');
  6. const callback = useCallback(() => {
  7. console.log(count);
  8. }, [count]);
  9. set.add(callback);
  10. return <div>
  11. <h4>{count}</h4>
  12. <h4>{set.size}</h4>
  13. <div>
  14. <button onClick={() => setCount(count + 1)}>+</button>
  15. <input value={val} onChange={event => setVal(event.target.value)}/>
  16. </div>
  17. </div>;
  18. }

每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

  1. import React, { useState, useCallback, useEffect } from 'react';
  2. function Parent() {
  3. const [count, setCount] = useState(1);
  4. const [val, setVal] = useState('');
  5. const callback = useCallback(() => {
  6. return count;
  7. }, [count]);
  8. return <div>
  9. <h4>{count}</h4>
  10. <Child callback={callback}/>
  11. <div>
  12. <button onClick={() => setCount(count + 1)}>+</button>
  13. <input value={val} onChange={event => setVal(event.target.value)}/>
  14. </div>
  15. </div>;
  16. }
  17. function Child({ callback }) {
  18. const [count, setCount] = useState(() => callback());
  19. useEffect(() => {
  20. setCount(callback());
  21. }, [callback]);
  22. return <div>
  23. {count}
  24. </div>
  25. }

参考

  1. useMemo与useCallback使用指南
  2. useMemo源码
  3. useCallback源码
  4. useEffect源码
  5. 一文轻松掌握react-hook(useState、useReducer、useEffect、useCallback、useMemo、useRef、useContext…)