熟悉 React 的同学们都知道,每次数据更新都会重新渲染 fiber 树,匹配渲染优先级组件及其所有子组件都会重新渲染,存在心智负担,可能开发中很多同学会掏出 useMemouseCallback, memo, pureComponent, sholdComponentUpdate 组合拳来优化避免组件重复渲染。

组件为什么会被重复渲染?

首先来段示例,当 App 组件触发会影响 Child 重新渲染。

function Child() {
  console.log('Child::render');
  return <div>child</div>

}

funcction App() {
  const [data, setData] = useState(0);
  return (
    <>
      <p>{ data }</p>

      <button onClick={handleClick}>click</button>

      <Child/>
    </>
  );

  function handleClick() {
    setData(data => data + 1);
  }
}

有哪些方法可以避免Child渲染呢?

const Child1 = () => {
  console.log('Child1::render');
  return <div>child</div>

}

const child1 = <Child1/>;

const Child6 = memo(() => {
  console.log('Child6::render');
  return <div>child6</div>

})

funcction App({ children, render }) {
  const [data, setData] = useState(0);
  return (
    <>
      <p>{ data }</p>

      <button onClick={handleClick}>click</button>

      { child1 }
      {
        useMemo(() => {
          console.log('Child2::render');
          return <div>child2</div>

        }, [])
      }
      {
        useState(() => {
          console.log('Child3::render');
          return <div>child3</div>

        })[0]
      }
      { children }
      { render }
      <Child6/>
    </>
  );

  function handleClick() {
    setData(data => data + 1);
  }
}

let child5 = () => {
    console.log('Child5::render');
    return <div>child5</div>

}

<App render={child5}>
  { React.createElement(() => {
    console.log('Chil4::render');
    return <div>child4</div>

  }) }
</App>

// 首次渲染
print -> star
  Child2::render
  Child3::render
  Child1::render
  Child4::render
  Child6::render
print -> end

通过点击 button 可以观察到控制台并无 Child 打印内容输出, 子组件并没有被重复渲染;

也就说除了常规包裹 useMemomemo 外,还有很多其他的方式也可以达到类似效果;

可能会有同学疑惑(后面会有解释)

React 组件会根据 state(useState|setState), props, context 来判断当前 fiber 是否可以复用

找到源码中 beginWork 方法,

beginWork 主要用于生成子 filber 以及打上对应 flags

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
  // current 存在 update更新
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // 判断新旧props是否全等 或者 上下文是否有变化
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged()
    ) {
      didReceiveUpdate = true;
      // 当渲染优先级不包含workInProgress的优先级,复用旧fiber
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      ...
      // 复用fiber
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        ...
        didReceiveUpdate = true;
      } else {
       ...
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
  }

乍一看不是很合理吗,那么为何App组件更新会导致Child组件刷新呢?

首先我们得知道workInProgress.pendingProps到底是啥?

// 源码中
const pendingProps = element.props;

// 那么这个element又是啥呢?
// 格式如下
{
  _owner:null
  _store:{validated: true}
  $$typeof:Symbol(react.element)
  key:null
  props:{}
  ref:null
  type:() => {\\n  console.log('Child::RENDER');\\n  return /*#__PURE...
}
// 是不是很熟悉,这就是常写得Jsx数据
// 可以打印看看
const Child1 = () => {
  console.log('Child1::render');
  return <div>child</div>;
};

const child1 = <Child1 />;
console.log(child1);

// 换个方式可以证明
let props1 = child1.props;
let props2 = child1.props;
console.log(props1 === props2);
print -> true

// props3 与 props4 生成得Jsx 不是同一引用因此不相等
let props3 = (<Child1 />).props;
let props4 = (<Child1 />).props;
console.log(props3 === props4);
print -> false

// 这个概念类似
const a = { "test": 1 };
const b = { "test": 1'};
console.log(a === b);
print -> false
const c = a; // "c" 仅仅是 "a" 的引用
console.log(a === c);
print -> true