Open IWSR opened 2 years ago
分析基于以下代码
import React, { useState, memo } from 'react'; const isEqual = (prevProps, nextProps) => { if (prevProps.number !== nextProps.number) { return false; } return true; } const ChildMemo = memo((props = {}) => { console.log(`--- memo re-render ---`); return ( <div> <p>number is : {props.number}</p> </div> ); }, isEqual); function Child(props = {}) { console.log(`--- re-render ---`); return ( <div> <p>number is : {props.number}</p> </div> ); }; export default function ReactMemo(props = {}) { const [step, setStep] = useState(0); const [count, setCount] = useState(0); const [number, setNumber] = useState(0); const handleSetStep = () => { setStep(step + 1); } const handleSetCount = () => { setCount(count + 1); } const handleCalNumber = () => { setNumber(count + step); } return ( <div> <button onClick={handleSetStep}>step is : {step} </button> <button onClick={handleSetCount}>count is : {count} </button> <button onClick={handleCalNumber}>numberis : {number} </button> <hr /> <Child step={step} count={count} number={number} /> <hr /> <ChildMemo step={step} count={count} number={number} /> </div> ); }
以下是按左到右依次点击 button 后的输出,可以看到 memo 包裹的组件只有在满足 compare 函数后才会发生更新,而另外一个每次点击都会重新 render
对 ChildMemo 打上断点,我们会进入到 memo 内
export function memo<Props>( type: React$ElementType, compare?: (oldProps: Props, newProps: Props) => boolean, ) { ... 删除了 dev 逻辑 const elementType = { $$typeof: REACT_MEMO_TYPE, type, compare: compare === undefined ? null : compare, }; ... 删除了 dev 逻辑 return elementType; }
逻辑很简单,对包裹的组件打上 REACT_MEMO_TYPE 这一标签,而这一步将会在构建 WIP 树时(beginWork)产生影响——也就是会进入到针对 MemoComponent 的逻辑内。
直接对 beginWork 内的 case MemoComponent打上断点,随意触发一个更新,便会进入对 memo 的逻辑(别看名字叫 updateMemoComponent,mount 阶段也是进这个函数创建节点的)
function updateMemoComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes, ): null | Fiber { // current 为空,对应的是 mount 阶段的事 // 当 current 为空时则创建对应的 fiber 节点(createFiberFromTypeAndProps),毕竟没有比较的对象直接创建就行 if (current === null) { const type = Component.type; ... ... 删除 dev 逻辑 const child = createFiberFromTypeAndProps( Component.type, null, nextProps, workInProgress, workInProgress.mode, renderLanes, ); child.ref = workInProgress.ref; child.return = workInProgress; workInProgress.child = child; return child; } ... 删除 dev 逻辑 // 这里因为存在 current 节点则进入更新逻辑 const currentChild = ((current.child: any): Fiber); // This is always exactly one child // 这里需要提一嘴,renderLanes 是当前的渲染优先级,也就是只有优先级与 renderLanes 相同的 update 才会在此次渲染中被处理 // 而每当一个 update 产生时便会将当前 update 的 lanes 标记到 root.pendingLanes 上,而 root.pendingLanes 上的最高优先级则会成为 renderLanes // 这里可以看一下 React Hooks: useState,我在那篇文章内有更详细的介绍 // 底下这个函数其实就是在当前的 fiber 节点上检查是否存在与 renderLanes 相同优先级的 update 实例,如果存在那么则意味着这个更新 // 必须在本轮 render 内处理掉,那么也就意味着即使传入的 props 没有发生变化(或者说 compare 函数返回 true),这个组件依然会被更新 // 那么我们就得到了 TLNR 中对应的第二条的结论 const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext( current, renderLanes, ); // 如果当前 fiber 上不存在需要立即被更新的 update, // 那么进入 if 内的逻辑 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 不存在,也就是 React.memo 的第二个参数没有传入的话就会使用默认的 shallowEqual // shallowEqual 是一个浅比较,只比较两个 props 的引用地址是否发生变化 compare = compare !== null ? compare : shallowEqual; if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { // 如果 compare 返回了 true,那么意味着 memo 包裹的节点可以被复用,于是调用 bailoutOnAlreadyFinishedWork 复用 current 树上的节点 // 这样就不需要重复生成节点,从而优化了性能 // bailoutOnAlreadyFinishedWork 这个函数的解析我后续补充到附录内 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } // React DevTools reads this flag. workInProgress.flags |= PerformedWork; // 运行到这里则说明当前 fiber 上存在需要立即被更新的 update // 那就只能重新生成节点了 const newChild = createWorkInProgress(currentChild, nextProps); newChild.ref = workInProgress.ref; newChild.return = workInProgress; workInProgress.child = newChild; return newChild; }
React.memo
TLNR
说明
分析基于以下代码
以下是按左到右依次点击 button 后的输出,可以看到 memo 包裹的组件只有在满足 compare 函数后才会发生更新,而另外一个每次点击都会重新 render
从 React.memo 开始
对 ChildMemo 打上断点,我们会进入到 memo 内
逻辑很简单,对包裹的组件打上 REACT_MEMO_TYPE 这一标签,而这一步将会在构建 WIP 树时(beginWork)产生影响——也就是会进入到针对 MemoComponent 的逻辑内。
beginWork 如何处理 MemoComponent
直接对 beginWork 内的 case MemoComponent打上断点,随意触发一个更新,便会进入对 memo 的逻辑(别看名字叫 updateMemoComponent,mount 阶段也是进这个函数创建节点的)