React性能优化之memo

230 阅读1分钟

没有Context

案例

import React from "react";
import { useState, useCallback } from "react";

const ChildComponent = React.memo(({ changeText }) => {
  console.log('子组件执行了');
  return (
      <p>test</p>
  )
});

const ParentComponent = () => {
  const [number, setNumber] = useState(1);

  const handleChange = () => {
    setNumber(number + 1);
  };

  // const changeText = (newText) => {
 
  // };
  //依赖项不变,引用不会变
  const changeText = useCallback(() => {

  },[])

  return (
    <>
      <button onClick={handleChange}>click me</button>
      <p>count: {number}</p>
      {/* style={{}} 还有这种写法 */}
      <ChildComponent changeText={changeText}/>
    </>
  )
};

export default ParentComponent



React.memo没有比较函数时默认检查 props 变更。

context(用了React Context API) 发生变化时,它仍会重新渲染,memo失效!!!

引用类型比较的是地址值

上源码(React 18)

function shallowEqual(objA, objB) {
  if (objectIs(objA, objB)) {
    return true;
  }

  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }

  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  } // Test for A's keys different from B.


  for (var i = 0; i < keysA.length; i++) {
    var currentKey = keysA[i];

    if (!hasOwnProperty.call(objB, currentKey) || !objectIs(objA[currentKey], objB[currentKey])) {
      return false;
    }
  }

  return true;
}
function updateSimpleMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
  // TODO: current can be non-null here even if the component
  // hasn't yet mounted. This happens when the inner render suspends.
  // We'll need to figure out if this is fine or can cause issues.
  {
    if (workInProgress.type !== workInProgress.elementType) {
      // Lazy component props can't be validated in createElement
      // because they're only guaranteed to be resolved here.
      var outerMemoType = workInProgress.elementType;

      if (outerMemoType.$$typeof === REACT_LAZY_TYPE) {
        // We warn when you define propTypes on lazy()
        // so let's just skip over it to find memo() outer wrapper.
        // Inner props for memo are validated later.
        var lazyComponent = outerMemoType;
        var payload = lazyComponent._payload;
        var init = lazyComponent._init;

        try {
          outerMemoType = init(payload);
        } catch (x) {
          outerMemoType = null;
        } // Inner propTypes will be validated in the function component path.


        var outerPropTypes = outerMemoType && outerMemoType.propTypes;

        if (outerPropTypes) {
          checkPropTypes(outerPropTypes, nextProps, // Resolved (SimpleMemoComponent has no defaultProps)
          'prop', getComponentNameFromType(outerMemoType));
        }
      }
    }
  }

  if (current !== null) {
    var prevProps = current.memoizedProps;

    if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress.ref && ( // Prevent bailout if the implementation changed due to hot reload.
     workInProgress.type === current.type )) {
      didReceiveUpdate = false; // The props are shallowly equal. Reuse the previous props object, like we
      // would during a normal fiber bailout.
      //
      // We don't have strong guarantees that the props object is referentially
      // equal during updates where we can't bail out anyway — like if the props
      // are shallowly equal, but there's a local state or context update in the
      // same batch.
      //
      // However, as a principle, we should aim to make the behavior consistent
      // across different ways of memoizing a component. For example, React.memo
      // has a different internal Fiber layout if you pass a normal function
      // component (SimpleMemoComponent) versus if you pass a different type
      // like forwardRef (MemoComponent). But this is an implementation detail.
      // Wrapping a component in forwardRef (or React.lazy, etc) shouldn't
      // affect whether the props object is reused during a bailout.

      workInProgress.pendingProps = nextProps = prevProps;

      if (!checkScheduledUpdateOrContext(current, renderLanes)) {
        // The pending lanes were cleared at the beginning of beginWork. We're
        // about to bail out, but there might be other lanes that weren't
        // included in the current render. Usually, the priority level of the
        // remaining updates is accumulated during the evaluation of the
        // component (i.e. when processing the update queue). But since since
        // we're bailing out early *without* evaluating the component, we need
        // to account for it here, too. Reset to the value of the current fiber.
        // NOTE: This only applies to SimpleMemoComponent, not MemoComponent,
        // because a MemoComponent fiber does not have hooks or an update queue;
        // rather, it wraps around an inner component, which may or may not
        // contains hooks.
        // TODO: Move the reset at in beginWork out of the common path so that
        // this is no longer necessary.
        workInProgress.lanes = current.lanes;
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      } else if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        // This is a special case that only exists for legacy mode.
        // See https://github.com/facebook/react/pull/19216.
        didReceiveUpdate = true;
      }
    }
  }

  return updateFunctionComponent(current, workInProgress, Component, nextProps, renderLanes);
}

bailoutOnAlreadyFinishedWork 是复用Fiber节点的逻辑

Context更新逻辑

案例

import React from 'react';

import { createContext, useContext, useState } from 'react';


const ThemeContext = createContext(null);
const App = () => {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <div onClick={() => {
        setTheme(theme == 'dark' ? 'light' : 'dark');
      }}>App</div>
      <Child />
    </ThemeContext.Provider>
  )

}

const Child = React.memo(() => {
  const theme = useContext(ThemeContext)
  console.log('虽然Memo包了一层,但还是更新了,因为是Context');
  return (
    <div>Child {theme}</div>
  )
})

export default App

源码部分

遗留问题

怎样解决Context更新引起的重新渲染问题???