useMemo和useCallback用法和源码

235 阅读2分钟

useMemo和useCallback都起到缓存的作用,seMemo缓存函数结果,useCallback是缓存函数。 两者的参数都是第一个为需要缓存的函数,第一个为依赖项数组(判断是否需要重新执行)

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把函数传入第一个参数,仅仅在a,b改变时,才会重新触发函数,假如函数中有大量的计算,接可以节省很大开销。

不使用useMemo

function Example() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    function getNum() {
        return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a+b)
    }
 
    return <div>
        <h4>总和:{getNum()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

可以看到每次当val改变时getNum都会重新进行计算,但是我们计算没用到Val,并不需要,造成了无效的性能开销。

使用useMemo

function Example() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    const getNum = useMemo(() => {
        return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a+b)
    }, [count])
 
    return <div>
        <h4>总和:{getNum()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

使用useMemo只有当count发生变化时,才会重新计算。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

useMemo和useCallback接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useMemo返回的是函数运行的结果,useCallback返回的是函数。

例如我们传给子组件一个函数,每次重新渲染的时候都会生成一个新函数,导致子组件重新渲染,useCallback

function Parent() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    const getNum = useCallback(() => {
        return Array.from({length: count * 100}, (v, i) => i).reduce((a, b) => a+b)
    }, [count])
 
    return <div>
        <Child getNum={getNum} />
        <div>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

const Child = React.memo(function ({ getNum }: any) {
    return <h4>总和:{getNum()}</h4>
})

使用useCallback之后,仅当count发生变化时Child组件才会重新渲染,而val变化时,Child组件是不会重新渲染的。

源码分析

useMemo和useCallback源码非常相似,首次挂载走mount,组件更新时走的是update。

// mount阶段就是获取到传入的回调函数和依赖数组,保存到hook的memorizedState中,然后返回回调函数。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

// update阶段
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  const hook = updateWorkInProgressHook();
 // 从hook的memorizedState中获取上次保存的值[callback, deps],
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps: Array<mixed> | null = prevState[1];
      // 比较新的deps和之前的deps是否相等
      if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 如果相等,返回memorized的callback
        return prevState[0];
      }
    }
  }
  // 如果deps发生变化,更新hook的memorizedState,并返回最新的callback
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

这立就只拿callback源码分析了,大致思想都一致 思想就是mount阶段获取传入的回调函数和依赖数组,保存到hook.memorizedState中,更新阶段将传入的依赖项跟上一次的相对比,决定是创建新的还是返回之前的值。