写点什么

React memo 的原理、实践与思考

  • 2024-03-21
    北京
  • 本文字数:4397 字

    阅读完需:约 14 分钟

前言

在 react 中,对一个组件进行点击事件等操作时,该组件以及该组件的子组件都会重新渲染。避免组件的重新渲染一般可以借助 React.memo、useCallback 等来实现。

什么是 memo

memo 原理

memo 类似于 class 中 pureComponent 的特性,用于在函数式组件的父组件中对子组件进行缓存,避免在父组件重新渲染时重新渲染子组件,只有在属性发生变化时重新渲染组件。


在 React v18.2.0 源码中,主要通过 packages/react-reconciler/src/ReactFiberBeginWork.new.js 的 updateMemoComponent 方法实现 memo 的特性。


function updateMemoComponent(  current: Fiber | null,  workInProgress: Fiber,  Component: any,  nextProps: any,  updateLanes: Lanes,  renderLanes: Lanes,): null | Fiber {  if (current !== null) {    // ...        const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(      current,      renderLanes,    );    if (!hasScheduledUpdateOrContext) {      // This will be the props with resolved defaultProps,      // unlike current.memoizedProps which will be the unresolved ones.      const prevProps = currentChild.memoizedProps;      // Default to shallow comparison      let compare = Component.compare;      compare = compare !== null ? compare : shallowEqual;      if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {        return bailoutOnAlreadyFinishedWork(          current,           workInProgress,           renderLanes        );      }    }  }
// ...}
复制代码


updateMemoComponent 首先会检查是否有已调度的更新或上下文更改。在存在更新时,他会去获取 memo 的 compare 方法,未自定义则取默认的比较方法 shallowEqual。去比较依赖数组中新老属性变化,确认不需要重新渲染时,会调用 bailoutOnAlreadyFinishedWork 方法来阻止组件的重新渲染。


在 memo 的应用中,一般需要结合 useMemo、useCallback 来配合处理依赖数组和子组件的传入属性,下面将介绍这两者的原理。

useMemo、useCallback 原理

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;}
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;}
复制代码


在 React v18.2.0 源码中,主要通过 packages/react-reconciler/src/ReactFiberHooks.js 的 updateCallback 和 updateMemo 方法实现 useCallback 与 useMemo 的特性。观察源码可以发现,useMemo 与 useCallback 的实现原理基本一致,均会通过 areHookInputsEqual 方法对依赖数组的各项进行比较。不同在于返回值上,useCallback 返回回调函数,useMemo 返回数据对象。useMemo 对属性进行缓存,通过对依赖数组进行监听,更新缓存的属性。

为什么使用 memo


memo 原理流程图


在这个流程中,在当前组件对应 fiber 节点的 tag 为 MemoComponent 且 没有其他更高优先级任务时,react.memo 会在新的 props 到达时,比较新旧 props 是否相等。如果相等,则不会重新渲染组件,直接返回之前的渲染结果。如果不相等,则重新执行渲染逻辑,更新组件的 DOM。这样可以避免不必要的组件重新渲染,提高性能。

memo 使用场景

memo 应用

让我们从一个示例出发:


export default const ChildCom = (props: any) => {  const { propParam1, propParam2, func1 } = props  console.log('child component update');  return (    <div onClick={func1}>      child component      {propParam1}      {propParam2.a}    </div>  )}
import ChildCom from './ChildCom'const MemoChildCom = React.memo(ChildCom)const App = () => { const [parentParam1, setParentParam1] = useState<number>(0) const [childParam1, setChildParam1] = useState<number>({ a: 2 }) const childFunc1 = () => { console.log("parent param1 val: ", parentParam1) } return ( <div className="App"> <button onClick={() => setParentParam1(parentParam1+1)}> observe child component update </button> <MemoChildCom propParam1={1} propParam2={childParam1} func1={childFunc1}/> </div> )}
复制代码


代码块 memo1 中,React.memo 有两个参数,第一个为需要缓存的组件,第二个是缓存规则的方法(非必传)。对组件进行缓存后,react 会根据缓存规则的实现方法,判断是否对子组件执行重新渲染。当第二参数未传时,react 会浅比较子组件的 props 对象里各属性的变化,若全部未更新,react 在 render 中,不会给此 fiber 打上更新的 tag,不会执行重新渲染。


注意的是,组件 ChildCom 中 props 属性 propParam2、func1 均为引用类型。在 button 被点击时,触发 setSate,App 组件会重新渲染,因此 childParam1 和 childFunc1 的引用地址会更新,在 react 进行浅比较会检测到更新, 使得 childCom 发生重新渲染。这种场景下,需要借助 useMemo 和 useCallBack 的特性。

useMemo、useCallback 应用

useCallback 应用


export default const ChildCom = (props: any) => {  const { propParam1, propParam2, func1 } = props  console.log('child component update');  return (    <div onClick={func1}>      child component      {propParam1}      {propParam2.a}    </div>  )}
import ChildCom from './ChildCom'const App = () => { const [parentParam1, setParentParam1] = useState<number>(0) const [childParam1, setChildParam1] = useState<number>({ a: 2 }) const childFunc1 = () => { console.log("parent param1 val: ", parentParam1) }
const callbackFunc1 = useCallback(childFunc1, [parentParam1]) const MemoChildCom = React.memo(ChildCom, (pre, cur) => { if(Object.is(pre, cur) && pre.propParam2.a !== cur.propParam2.a) { return true } else return false }) return ( <div className="App"> <button onClick={() => setParentParam1(parentParam1+1)}> observe child component update </button> <MemoChildCom propParam1={1} propParam2={childParam1} func1={callbackFunc1}/> </div> )}
复制代码


观察代码块 memo1 中可发现,两个引用类型的变量,其依赖于 parentParam1 和 childParam1 的属性 a。首先借助 useCallback 对方法进行缓存,并在 childCom 组件的调用上,参数 func1 传入 callbackFunc1。另外,在 React.memo 的比较策略的方法实现中,需要额外比较引用类型的变更。


useMemo 应用


useMemo 对于属性的缓存特性,其更多用于对复杂对象的处理并用于部分 hooks 的依赖数组上。例如 useEffect 的依赖数组中,直接监听大对象的行为是绝对要避免的,监听的对象中未参与的属性在 useState 发生变化后会引起 useEffect 不必要的执行,甚至发生死循环。这种场景一般可以借助 useMemo 进行精细化的监听操作。如下示例:


type TAddressDTO = {  id?: string  provinceId: number  provinceName: string  cityId: number  cityName: string  countyId: number  countyName: string  townId?: number  townName?: string  address: string  fullAddress?: string  type: number  defaultFlag?: number  name: string  mobile?: string  mobileITC?: string  encryptMobile?: string  phone?: string  extNumber?: string  encryptPhone?: string  company?: string  shortName?: string}const memoSendAddress = useMemo(() => {  const {    provinceId = '',    cityId = '',    countyId = '',    townId = '',    address = '',  } = storeSendAddress || {}  return `${provinceId}${cityId}${countyId}${townId}${address}`}, [storeSendAddress])useEffect(() => {  // useEffect logic}, [memoSendAddress])
复制代码


代码块 useMemo1 中,useEffect 针对地址对象的监听,直接监听 storeSendAddress 的话,地址大对象中任意属性的变更均未造成 useEffect logic 的执行。但实际地址对象 TAddressDTO 需要监听的属性只有各级地址。通常 useEffect logic 中会包含接口调用、数据存取等异步任务,通过 useMemo 进行筛选是很有必要的。


需要注意的是,useMemo 第一个参数的返回值,需要为基础数据类型,否则 memoChildParam1 在 App 组件重新渲染后依然会返回新的引用地址造成 MemoChildCom 的重新渲染。

memo 应用中的坑

实际应用中,memo 的使用并不频繁,个人有如下的几点看法。


1.在代码的可维护性上,过多的使用 React.memo 对组件进行缓存并同时自定义比较方法,无形中增加了代码的复杂度,对于之后的维护上,对于他人甚至于自己,都会带来额外理解的时间;


2.对于 react 性能的疑虑上,diff 算法会作为最后的关卡,去优化真实 DOM 的渲染过程;


3.父组件重新渲染触发子组件的 render,可以避免相当一部分的 bug。笔者实际开发遇到的场景中,在例如列表渲染的诸多场景,列表的每一项往子组件传入的对象往往比较大,当对象中深层的某个属性发生改变,但由于其他 state 的变化触发父组件的重新渲染,这种触发子组件重新渲染的场景是普遍存在的。

小结

本文主要讲述了 React 中组件缓存中 memo 的原理和一般应用,对于组件重新渲染的必要与否,更需要我们根据实际场景,测量分析缓存的消耗与节约的成本及潜在风险之间的关系,避免负优化的发生。毕竟,react 的 diff 算法会为我们最后把关。

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
React memo的原理、实践与思考_京东科技开发者_InfoQ写作社区