banner
IWSR

IWSR

我永远喜欢志喜屋梦子!

React.memo

TLNR#

  1. React.memo 透過對傳入的組件打上 REACT_MEMO_TYPE 的標籤後使得在每一次更新前對傳入的 props 進行淺比較(或者透過 compare 函數對新舊 props 進行比較),來決定是否重用原來的 fiber 組件
  2. 如果當前組件內存在更新,那麼 memo component 會跳過比較,直接生成新的 fiber

說明#

分析基於以下代碼

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

Nov-24-2022 00-07-23

從 React.memo 開始#

對 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 如何處理 MemoComponent#

直接對 beginWork 內的 case MemoComponent 打上斷點,隨意觸發一個更新,便會進入對 memo 的邏輯(別看名字叫 updateMemoComponent,mount 階段也是進這個函數創建節點的)

image
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;
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。