触摸 react 的命门 - 值的相等性比较(下篇)

200 阅读3分钟

回顾

在上一个篇文章: 触摸 react 的命门 - 值的相等性比较(上篇)中,我们已经指出, react 的源码中,对单个值的比较算法是通过 objectIs 这个函数来实现的:

// packages/shared/objectIs.js
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

/**
 * inlined Object.is polyfill to avoid requiring consumers ship their own
 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
 */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
  );
}

const objectIs: (x: any, y: any) => boolean =
  // $FlowFixMe[method-unbinding]
  typeof Object.is === "function" ? Object.is : is;

export default objectIs;

从上面的注释和实现代码可以看出,react 是优先考虑使用原生的 Object.is 函数来判断两个值是否相等的。只有当前环境没有提供 Object.is 函数时,才会使用 polyfill objectIs 函数。至于 polyfill objectIs 函数的实现原理,我在上一篇文章的末尾做了详细的介绍,这里就不再赘述了。

React 中值比较的关键场景

hook 依赖比较

在 react 中,我们常用的涉及到依赖数组的 hook 有:

  • useMemo()
  • useCallback()
  • useEffect()
  • useLayoutEffect()

而众所周知的是,只有 react 觉得依赖数组中的值发生变化时,相应的代码才会被再次执行。可以这么说,我们的应用代码就是这样被 react 扼住了命运的咽喉。通过研究以上 hook 的源码,我们不难发现,react 是使用了一个叫做 areHookInputsEqual() 的函数来判断依赖数组中的值是否发生变化的。下面直接抛出源码来看看:

// packages/react-reconciler/src/ReactFiberHooks.js

// 这是 useEffect 在 update 阶段的实现
function updateEffectImpl(
  fiberFlags: Flags,
  hookFlags: HookFlags,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const effect: Effect = hook.memoizedState;
  const inst = effect.inst;

  // currentHook is null on initial mount when re-rendering after a render phase
  // state update or for strict mode.
  if (currentHook !== null) {
    if (nextDeps !== null) {
      const prevEffect: Effect = currentHook.memoizedState;
      const prevDeps = prevEffect.deps;
      // $FlowFixMe[incompatible-call] (@poteto)
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        hook.memoizedState = pushSimpleEffect(
          hookFlags,
          inst,
          create,
          nextDeps
        );
        return;
      }
    }
  }

  currentlyRenderingFiber.flags |= fiberFlags;

  hook.memoizedState = pushSimpleEffect(
    HookHasEffect | hookFlags,
    inst,
    create,
    nextDeps
  );
}

// 这是 useLayoutEffect 在 update 阶段的实现,它的具体实现代理给了 `updateEffectImpl`
function updateLayoutEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
): void {
  return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}

// 这是 useCallback 在 update 阶段的实现
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;
}

// 这是 useMemo 在 update 阶段的实现
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];
    }
  }
  const nextValue = nextCreate();
  if (shouldDoubleInvokeUserFnsInHooksDEV) {
    setIsStrictModeForDevtools(true);
    try {
      nextCreate();
    } finally {
      setIsStrictModeForDevtools(false);
    }
  }
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

对于缓存类的 hook:useMemouseCallback 来讲,如果 react 通过 areHookInputsEqual() 来比较前后依赖数组的结果是相等的话,那么就缓存命中,hook 就会返回上一次的值;否则,就用当前值替换上一次的值,然后返回当前值。

对于缓存类的 hook,它的值(memoizedState)是一个 tuple:第一值是缓存起来的值,第二值是依赖数组。

对于 effect 类的 hook: useLayoutEffectuseEffect,如果 react 通过 areHookInputsEqual() 来比较前后依赖数组的结果是不相等的话,react 才会给它们对应的 hook 对象打上一个标签: HookHasEffect。在 commit 阶段,react 会遍历 effect 对象链表, 只有被打上 HookHasEffect 标签的 effect 对象里面的 create 函数和 destroy 函数才会被执行。

更深入的原理阐述请查看我的文章《全网最新,最全面,也是最深入剖析 useEffect() 原理的文章, 没有之一 API 简介 函数签名 useEffec - 掘金

从上面的分析我们可以得知,利用 areHookInputsEqual()来做值的比较的结果决定了我们的应用代码的预期行为的。

areHookInputsEqual() 的函数的实现又是长什么样子呢?下面我们就来一起来看看吧!

function areHookInputsEqual(nextDeps, prevDeps) {
    {
      if (ignorePreviousDependencies) {
        // Only true when this component is being hot reloaded.
        return false;
      }
    }

    if (prevDeps === null) {
      {
        error('%s received a final argument during this render, but not during ' + 'the previous render. Even though the final argument is optional, ' + 'its type cannot change between renders.', currentHookNameInDev);
      }

      return false;
    }

    {
      // Don't bother comparing lengths in prod because these arrays should be
      // passed inline.
      if (nextDeps.length !== prevDeps.length) {
        error('The final argument passed to %s changed size between renders. The ' + 'order and size of this array must remain constant.\n\n' + 'Previous: %s\n' + 'Incoming: %s', currentHookNameInDev, "[" + prevDeps.join(', ') + "]", "[" + nextDeps.join(', ') + "]");
      }
    }

    for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
      if (objectIs(nextDeps[i], prevDeps[i])) {
        continue;
      }

      return false;
    }

    return true;
  }

上面源码中的 is() 指向的就是 objectIs() 函数。于此同时,我们可以看到,只有同时满足以下的两个条件,react 才会认为前后的两个依赖数是相等的:

  • 依赖数组的长度是相等的
  • 依赖数组中的每一项都是相等的

在比较数组特定位置的两个依赖数组元素的值是否相等的时候,采用正是我们在上一篇文章中提到过的 objectIs 函数。该函数在 react 源码中的实现在本文的开头处已经给出了,这里就不再赘述了。

2. 主动更新下的避免 re-render

对于组件的 re-render 的触发条件,需要分三种情况来看:

  • 继承自 React.Component 的组件
  • 继承自 React.PureComponent 的组件
  • 函数式组件

2.1 继承自 React.Component 的组件

对于这种情况,通过研究源码和做试验,结果有点惊呆我了。真的想不到 react 对于传统的类组件的渲染性能把控是这么松散的。假设,我们现在有这样的类组件:

import { Component } from "react";
export class Test extends Component<any, { count: number }> {
  constructor(props: any) {
    super(props);
    this.state = {
      count: 1,
    };
  }

  render() {
    return (
      <button
        onClick={() => {
          this.setState({ count: 1 });
        }}
      >
        count: {this.state.count}
      </button>
    );
  }

  componentDidUpdate() {
    console.log("componentDidUpdate");
  }
}

当我们调用 this.setState({ count: 1 }) 的时候,实际上是调用 classComponentUpdater 的 enqueueSetState() 方法。从这个方法往调用栈一路向下追溯,直到真正进入 render 阶段之前,我都没有发现 react 有任何的值或者引用相等性的比较。也就是说 react 在调度阶段开了一路绿灯。

在 react 中,一次的更新请求由三个阶段组成:调度阶段,render 阶段 和 commit 阶段。

进入 render 阶段后,我在最可能发生值比较的地方(checkShouldComponentUpdate())打了个断点:

image.png

checkShouldComponentUpdate()的返回值是作为帧函数updateClassInstance() 里面 shouldUpdate 的变量的值,这个变量决定了这个组件是否会触发 re-render。

下面我们来看看 checkShouldComponentUpdate() 的源码:

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext
) {
  const instance = workInProgress.stateNode;

  if (typeof instance.shouldComponentUpdate === "function") {
    let shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext
    );

    return shouldUpdate;
  }

  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

上面源码的逻辑一目了然。所以,我们可以抛出结论了:对于通过继承 Component 类来创建且没有实现 shouldComponentUpdate方法的组件,一旦调用 this.setState(),不管 state 的值或者引用有没有变化,都会触发 re-render

事实也是这样,你不妨可以看看我用于试验的 demo:re-render 之 Component VS PureComponent - StackBlitz

按理说,我下面的两个测试 case 都不应该导致 re-render:

// case 1: 值是相等的
this.setState({ count: 1 });

// case 2: 引用是相等的
this.setState(this.state);

但是实际上这两个 case 都会导致 re-render,从 componentDidUpdate() 生命周期方法别调用我们可以反推这一点。

从上面的探索,我可以得到一个认知,如果你非要使用 class 组件且你在乎渲染性能,那么你必须要自己实现 shouldComponentUpdate() 方法。

2.2 继承自 React.PureComponent 的组件

react 的官方文档 PureComponent – React明确指出,React.PureComponentReact.Component的一个子类,不同之处仅仅在于这个子类内置了一个 shouldComponentUpdate() 方法,仅此而已。从源码来看,确实也是如此:

/**
 * Convenience component with default shallow equality check for sCU.
 */

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context; // If a component has string refs, we will assign a different object later.

  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

var pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent; // Avoid an extra prototype jump for these methods.

assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

在 PureComponent 类型的组件中调用 this.setState() 的时候,react 会调用 Component.prototype.enqueueSetState() 方法,因而在这个 case 下,整个调用栈跟在 Component 类型的组件调用this.setState()是一样的。最终还是来到了 checkShouldComponentUpdate() 方法里面。

从源码来看,checkShouldComponentUpdate() 的内部实现中的这个代码分支:

if (ctor.prototype && ctor.prototype.isPureReactComponent) {
  return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
}

确实证实了官方文档所说不虚:

PureComponent is a subclass of Component and supports all the Component APIs. Extending PureComponent is equivalent to defining a custom shouldComponentUpdate method that shallowly compares props and state.

而,shallowEqual 实现的内核还是 objectIs 函数:

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */

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;
}

从上面的实现我们可以得出,只有以下的几种情况,浅比较的结果才是 true:

  • objAobjB 都是相同的基础类型
  • objAobjB 都是 NaN
  • objAobjB 都是指向同一个引用
  • objAobjB 不是同一个引用,但是所拥有的自有属性的个数,以及相同的属性名所指向的值基于Object.is() 来比较的结果是 true

其他的情况,浅比较的结果都是 false。所以,最后,还是回到 react 的命门上面来了 - 单个值的比较算法Object.is()

想体验 PureComponent 与 Component 的差异,还是到re-render 之 Component VS PureComponent - StackBlitz这里体验。

不够话说回来,自从进入后 react hook 时期后,官方已经不推荐使用 class 组件了。这里之所以还梳理这两个场景,是为了让读者更立体地了解 react 的命门。

函数式组件

函数式组件才是 react 的未来。那 react 的命门在函数式组件中的表现是什么呢?下面让我们一起来看看。我们用到 demo 如下:

import * as React from "react";
import { useState } from "react";
import { createRoot } from "react-dom/client";

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount((count) => {
      if (count < 2) return count + 1;
      return count;
    });
  }

  return <button onClick={handleClick}>Pressed {count} times</button>;
}

const root = createRoot(document.querySelector("#root"));
root.render(<Counter />);

在继续深入之前,这里要提一提 react 在渲染行为方面的一个「怪异现象」 - 重复设置一个相同的值的时候,第一次重复仍然会触发一个 re-render。这显然是不符合常理的。很多人都对此有疑问:

最权威的解释来自于老版的 react 文档:Hooks API Reference – React

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
**Note that React may still need to render that specific component again before bailing out. **That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

在本文中,我不会发散去讲这个旁枝。我只是聚焦这么一个场景:在非第一次重复(排除怪异行为),主动调用 setState 去设置相同的值的时候,值的相等性比较是如何影响 react 的 re-render 行为。

在我以往关于 hook 的文章中介绍过,functional component 的 setState() 这个 API 的真正实现是 dispatchSetState(),它的源码如下:

function dispatchSetState(fiber, queue, action) {
  {
    if (typeof arguments[3] === "function") {
      error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          "second callback argument. To execute a side effect after " +
          "rendering, declare it in the component body with useEffect()."
      );
    }
  }

  var lane = requestUpdateLane(fiber);
  var update = {
    lane: lane,
    action: action,
    hasEagerState: false,
    eagerState: null,
    next: null,
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    var alternate = fiber.alternate;

    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      var lastRenderedReducer = queue.lastRenderedReducer;

      if (lastRenderedReducer !== null) {
        var prevDispatcher;

        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current =
            InvalidNestedHooksDispatcherOnUpdateInDEV;
        }

        try {
          var currentState = queue.lastRenderedState;
          var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.

          update.hasEagerState = true;
          update.eagerState = eagerState;

          if (objectIs(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            // TODO: Do we still need to entangle transitions in this case?
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }

    var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

    if (root !== null) {
      var eventTime = requestEventTime();
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }

  markUpdateInDevTools(fiber, lane);
}

在本小节所研究的场景下,代码执行会进入下面这个分支:

if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
    ....
}

最终我们会来到 react 的命门:

if (objectIs(eagerState, currentState)) {
  // Fast path. We can bail out without scheduling React to re-render.
  // It's still possible that we'll need to rebase this update later,
  // if the component re-renders for a different reason and by that
  // time the reducer has changed.
  // TODO: Do we still need to entangle transitions in this case?
  enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
  return;
}

可以看出,在这里,react 是基于 Object.is() 算法对单个值进行比较,而不是更好的 shallowEqual() 算法。

enqueueConcurrentHookUpdateAndEagerlyBailout() 函数虽然还是入队了一个 update 对象到队列中,但是它没有发起一次更新的调度,所以更不会进入 render 阶段。代码的执行流就戛然而止。通过这种方式, react 来阻止了不必要的 re-render。

在上面,我提到了, shallowEqual() 算法比 Object.is() 算法更好。为什么?我们不妨考虑下面的应用代码:

import * as React from "react";
import { useState } from "react";
import { createRoot } from "react-dom/client";

function Counter() {
  const [count, setCount] = useState({
    current: 0,
  });

  function handleClick() {
    setCount({
      current: 0,
    });
  }

  return <button onClick={handleClick}>Pressed {count.current} times</button>;
}

const root = createRoot(document.querySelector("#root"));
root.render(<Counter />);

在这个例子中,react 会认为值是不相等的,每点击一次按钮就会触发 re-render。这是因为 Object.is() 算法认为两个不同的引用是不相等的,而不去管你语义上值是否相等。而这不是我们希望的结果,所以在这里,我觉得使用 shallowEqual() 算法是更好的选择。

3. 被动更新下的避免 re-render

在性能优化的这个场景下,「bailout」是一个很重要的概念,因为它能够帮助 react 减少不必要的重渲染。

「重渲染」可以简单理解为「调用组件的 render 函数」

在 react 的源码中,bailout 逻辑出现的频率很高,在很多地方的源码或者注释中都有提到。下面简单罗列一下蕴含 bailout 逻辑的函数或者变量:

  • getNextLanes()
  • enqueueConcurrentHookUpdateAndEagerlyBailout()
  • bailoutHooks()
  • bailoutOnAlreadyFinishedWork()
  • checkScheduledUpdateOrContext()
  • attemptEarlyBailoutIfNoScheduledUpdate()
  • bubbleProperties() - didBailout变量

bailout 逻辑中一般包含三种条件:

  • 是否依赖了 context,且 context 的值发生了变化
  • 是否触发了 state 更新,且更新的优先级处于当前的 renderLane 之中
  • props 的值是否发生了变化

只有同时满足上面的三个条件时,才会触发 bailout。因为本文关注的是「值的比较」对 react 行为的影响,所以,我只会讲第三种条件的情况:props 的值是否发生了变化。而在这个情况下,最典型的一个应用场景就是 React.memo API。所以,我就来讲一讲在内部实现中, 「值的比较」 是如何帮助 React.memo() 来实现 bailout 的 。

首先,React.memo() 的源码实现很简单:

  var REACT_MEMO_TYPE = Symbol.for('react.memo');

  function memo(type, compare) {
    {
      if (!isValidElementType(type)) {
        error('memo: The first argument must be a component. Instead ' + 'received: %s', type === null ? 'null' : typeof type);
      }
    }

    var elementType = {
      $$typeof: REACT_MEMO_TYPE,
      type: type,
      compare: compare === undefined ? null : compare
    };

    // 省略的部分跟 displayName 有关,不重要,不妨跳过
    ...
    return elementType;
  }

$$typeof 是 react element/组件的分类标签。在这里,React.memo()的作用无非就是给原始组件包一层,并且打上标记来表示:这是使用 React.memo() API 包裹过的组件。React.memo() 真正发挥作用的时机是在 render 阶段的 begin work 的时候。

在 react 应用的 mount 阶段,memorized 组件所对应的 fiber 节点会在父 fiber 节点的 begin work 阶段被创建出来。创建之初打上的 work tag 为 MemoComponent

根据 react element 的数据结构来打 work tag 的逻辑代码在 createFiberFromTypeAndProps() 这个函数里面

然后,轮到自己的 begin work 的时候,根据「是否定制了 compare 函数」来决定是否把自己的 work tag 修正为 SimpleMemoComponent

  function updateMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
    if (current === null) {
      var type = Component.type;

      if (isSimpleFunctionComponent(type) && Component.compare === null && // SimpleMemoComponent codepath doesn't resolve outer props either.
      Component.defaultProps === undefined) {

       ...
        workInProgress.tag = SimpleMemoComponent;
       ...
        return updateSimpleMemoComponent(current, workInProgress, resolvedType, nextProps, renderLanes);
      }

    ...
    }

    ...
  }

到这里,一个 memorized 组件在 react 的内部是会被区分为两种类型:

因为在现代的 react 组件中,Component.defaultProps 这种写法基本上被淘汰了。所以可以说,主要是根据是否定制了 compare 函数来做区分。

  • MemoComponent
  • SimpleMemoComponent

bailout/跳过不必要的重渲染,理所当然是发生在 react 应用的 update 阶段。在 render 阶段的 begin work 步骤中,无论是 updateMemoComponent() 还是 updateSimpleMemoComponent(),它们的 bailout 逻辑代码框架都是一样的:

// `updateSimpleMemoComponent()` 的 bailout 逻辑
function updateSimpleMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
    ...
    if (current !== null) {
      var prevProps = current.memoizedProps;

      if (shallowEqual(prevProps, nextProps) && current.ref === workInProgress.ref && (
       workInProgress.type === current.type )) {
        didReceiveUpdate = false;

        workInProgress.pendingProps = nextProps = prevProps;

        if (!checkScheduledUpdateOrContext(current, renderLanes)) {
          ...
          return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
        } else if (...) {
          ...
        }
      }
    }
}
// `updateMemoComponent()` 的 bailout 逻辑
function updateMemoComponent(current, workInProgress, Component, nextProps, renderLanes) {
 if (current === null) {
   ...
   return ...
 }
  ...
  if (!hasScheduledUpdateOrContext) {
      var prevProps = currentChild.memoizedProps;

      var compare = Component.compare;
      compare = compare !== null ? compare : shallowEqual;

      if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    }
  ...
}

从上面的源码中,我们可以看出,memorized 组件的如果需要 bailout(调用 bailoutOnAlreadyFinishedWork())),是需要同时满足上面已经提过的三个条件的:

  • 没有依赖 context 或者依赖了 context,但是 context 的值没有发生了变化;
  • 没有触发 state 更新,或者触发了 state 更新,但是优先级并不处于当前的 renderLane 之中;
  • props 的值没有发生了变化;

聚焦到我们需要探究的主题:props 值的比较结果是如何影响 react memo 组件行为表现的。我们可以清晰地看到,Object.is() 算法在这里所扮演的「命门」角色 - shallowEqual() 的底层依赖还是 Object.is() 算法。

4. 总结

通过深入源码讲解 react 来三个关键的「值的比较」场景:

  • hook 依赖比较
  • 主动更新下的避免 re-render
  • 被动更新下的避免 re-render

我们发现「值的比较」是 react 行为表现的命门。而纵观 react 打包后的源码(全局搜索:ObjectIs()),值的比较所采用的算法有:

  • ObjectIs()
  • shallowEqual()
  • areHookInputsEqual()

ObjectIs() 是后两者的基础。于此同时,我们也知道 ObjectIs() 只是原生 Object.is() 的 polyfill。

  • 「值的比较」是 react 行为表现的命门;
  • 而原生的 Object.is() 算法,是 react 中「值的比较」的命门。

综上所述,我们可以说:原生的 Object.is() 算法,是 react 中「值的比较」的命门中的命门 - 简称为「终极命门」