跳至主要內容

🤔useMemo还可以这样用?useCallback:糟了,我成替身了!

萌萌哒草头将军大约 4 分钟前端JavaScriptReact

文章首发公众号:萌萌哒草头将军,最近关注有🎁,欢迎关注!

最近在研究React的源码,然后,我就悟了!

image.png

💡推荐阅读

🎉干货满满,React设计原理,藏在源码里的五指山🎉open in new window

💎 开门见山

请看👇的代码:你觉得可以按预期运行吗?

import { useMemo, useState } from 'react'

function App() {

  const [count, setCount] = useState(0)

  const onClick = useMemo(() => {
      return () => setCount((count) => count + 1)
  }, [])
  
  useMemo(() => console.log(count), [count])

  return (
    <div className="App">
      <button onClick={onClick}>
          count is {count}
        </button>
    </div>
  );
}

export default App;

答案是完全可以!

💎 分析

🚗 用法分析

他们都接收两个参数,useXxx(callback, [...deps])

  • 👉第一个参数callback是回调函数

  • 👉第二个参数deps是依赖项

不同的是当依赖项发生改变时

  • 🚆useCallback会重新创建回调函数,以保证每次调用都是最新值。并缓存这个函数

  • 🚆useEffect回调函数会重新执行

  • 🚆useMemo回调函数会重新执行,并缓存返回值。

根据useMemo返回值的不同,可以模拟出不同的效果:

  • 👉当返回值是个函数时,它useCallback和是完全等效的。

  • 👉当没有返回值或者不管返回值时,它useEffect和部分功能是等效的

这是因为,它不会像useEffect一样,对返回值做处理。也就是说,它无法模拟unMounted生命周期函数。

就是这么简单的原因,上面的代码会执行成功。

🚗 源码分析

这部分是选读,如果你对源码感兴趣,可以阅读这块。

🚆 useMemo源码逻辑

  • 👉注册hook状态

  • 👉此时是mounted阶段,调用mountMemo

  • 👉将注册的callbackdeps拿出来

  • 👉执行callback,并将执行结果和deps缓存在当前hook的状态上

  • 👉deps发生改变,进入update阶段,调用updateMemo

  • 👉取出当前的hook状态,拿到callbackdeps,再从当前hook拿到上次的deps

  • 👉比较前后两次的deps,如果一致,直接返回当前的状态值

  • 👉否则重新执行callback,保持返回值,并将该值最为最新的状态值和deps一起保存起来

function mountMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 拿到当前的hook状态
  const hook = mountWorkInProgressHook();
  // 拿到当前的hook依赖项
  const nextDeps = deps === undefined ? null : deps;
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
  }
  // 执行回调函数
  const nextValue = nextCreate();
  // 缓存回调函数返回值和依赖
  hook.memoizedState = [nextValue, nextDeps];
  // 返回返回值
  return nextValue;
}

function updateMemo<T>(
  nextCreate: () => T,
  deps: Array<mixed> | void | null,
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  // Assume these are defined. If they're not, areHookInputsEqual will warn.
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 如果前后依赖相同时,直接返回当前值
      return prevState[0];
    }
  }
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    nextCreate();
  }
  // 否则重新计算赋值,并返回最新值
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

🚆 useCallback源码逻辑

  • 👉注册hook状态

  • 👉此时是mounted阶段,调用mountCallback

  • 👉将注册的callbackdeps拿出来

  • 👉将callbackdeps缓存在当前hook的状态上

  • 👉deps发生改变,进入update阶段,调用updateCallback

  • 👉取出当前的hook状态,拿到callbackdeps,再从当前hook拿到上次的deps

  • 👉比较前后两次的deps,如果一致,直接返回当前的状态值

  • 👉否则重新将callback做为最新的状态值和deps一起保存起来

function mountCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null
): T {
  // 获取当前hook状态
  const hook = mountWorkInProgressHook();
  // 获取当前hook依赖项
  const nextDeps = deps === undefined ? null : deps;
  // 缓存回调函数和依赖
  hook.memoizedState = [callback, nextDeps];
  // 返回回调函数
  return callback;
}

function updateCallback<T>(
  callback: T,
  deps: Array<mixed> | void | null
): T {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  if (nextDeps !== null) {
    const prevDeps: Array<mixed> | null = prevState[1];
    if (areHookInputsEqual(nextDeps, prevDeps)) {
      // 如果依赖项相同时,直接返回当前值
      return prevState[0];
    }
  }
  // 否则重新赋值,并返回最新值
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

从源码上看,useCallbackuseMemo的实现十分类似,唯一的不同之处是:useMemo在依赖项发生变化时会缓存回调函数的返回值。

💎 总结

useCallbackuseMemo都是缓存中间状态,

不同的是useMemo可以缓存任何类型的值,useCallback仅仅缓存函数。所以开头的例子可以按预期运行。

好了,今天的分享比较简单,但是希望可以帮你理解地更深一点。

下篇我们继续聊hook