前言:
此刻姐妹们对“react reconciler”大概比较重视,我们都想要剖析一些“react reconciler”的相关内容。那么小编同时在网上汇集了一些有关“react reconciler””的相关知识,希望大家能喜欢,兄弟们快快来了解一下吧!前言
在react中,对一个组件进行点击事件等操作时,该组件以及该组件的子组件都会重新渲染。避免组件的重新渲染一般可以借助 React.memo、useCallback 等来实现。
什么是 memomemo 原理
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 算法会为我们最后把关。
标签: #react reconciler