回顾
在上一个篇文章: 触摸 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:useMemo 和 useCallback 来讲,如果 react 通过 areHookInputsEqual() 来比较前后依赖数组的结果是相等的话,那么就缓存命中,hook 就会返回上一次的值;否则,就用当前值替换上一次的值,然后返回当前值。
对于缓存类的 hook,它的值(
memoizedState)是一个 tuple:第一值是缓存起来的值,第二值是依赖数组。
对于 effect 类的 hook: useLayoutEffect和 useEffect,如果 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())打了个断点:
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.PureComponent 是 React.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:
objA和objB都是相同的基础类型objA和objB都是NaNobjA和objB都是指向同一个引用objA和objB不是同一个引用,但是所拥有的自有属性的个数,以及相同的属性名所指向的值基于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。这显然是不符合常理的。很多人都对此有疑问:
- reactjs - Why React needs another render to bail out state updates? - Stack Overflow
- javascript - What are the differences when re-rendering after state was set with Hooks compared to the class-based approach? - Stack Overflow
- javascript - React hooks useState setValue still re-render one more time when value is equal - Stack Overflow
- Bug: Functional component re-render when same state · Issue #19305 · facebook/react
- Unexpected function component call using useState · Issue #17672 · facebook/react
- useState not bailing out when state does not change · Issue #14994 · facebook/react
最权威的解释来自于老版的 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 函数来做区分。
MemoComponentSimpleMemoComponent
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 中「值的比较」的命门中的命门 - 简称为「终极命门」。