乐趣区

关于javascript:React内部的性能优化没有达到极致

大家好,我卡颂。

对于如下这个常见交互步骤:

  1. 点击按钮,触发 状态更新
  2. 组件render
  3. 视图渲染

你感觉哪些步骤有 性能优化的空间 呢?

答案是:1 和 2。

对于 步骤 1 ,如果 状态 更新前后没有变动,则能够略过剩下的步骤。这个优化策略被称为eagerState

对于 步骤 2 ,如果组件的子孙节点没有状态变动,能够跳过子孙组件的render。这个优化策略被称为bailout

看起来 eagerState 的逻辑很简略,只须要比拟 状态更新前后是否有变动

然而,实际上却很简单。

本文通过理解 eagerState 的逻辑,答复一个问题:React的性能优化达到极致了么?

欢送退出人类高质量前端框架群,带飞

一个奇怪的例子

思考如下组件:

function App() {const [num, updateNum] = useState(0);
  console.log("App render", num);

  return (<div onClick={() => updateNum(1)}>
      <Child />
    </div>
  );
}

function Child() {console.log("child render");
  return <span>child</span>;
}

在线 Demo 地址

首次渲染, 打印:

App render 0
child render 

第一次点击div, 打印:

App render 1
child render 

第二次点击div, 打印:

App render 1

第三、四 …… 次点击div,不打印

第二次 点击中,打印了 App render 1,没有打印child render。代表App 的子孙组件没有render,命中了bailout

第三次及之后 的点击,什么都不打印,代表没有组件render,命中了eagerState

那么问题来了,明明第一、二次点击都是执行updateNum(1),显然状态是没有变动的,为什么第二次没有命中eagerState

eagerState 的触发条件

首先咱们须要明确,为什么叫eagerState(急切的状态)?

通常,什么时候能获取到 最新状态 呢?组件 render 的时候。

当组件 renderuseState 执行并返回 最新状态

思考如下代码:

const [num, updateNum] = useState(0);

useState执行后返回的 num 就是 最新状态

之所以 useState 执行时能力计算出 最新状态 ,是因为 状态 是依据 一到多个更新 计算而来的。

比方,在如下点击事件中触发 3 个更新:

const onClick = () => {updateNum(100);
  updateNum(num => num + 1);
  updateNum(num => num * 2);
}

组件 rendernum 最新状态 应该是多少呢?

  • 首先 num 变为 100
  • 100 + 1 = 101
  • 101 * 2 = 202

所以,useState会返回 202 作为num 的最新状态

理论状况会更简单,更新 领有本人的 优先级 ,所以在render 前不能确定 到底是哪些更新会参加状态的计算

所以,在这种状况下组件必须 renderuseState 必须执行能力晓得 num 的最新状态 是多少。

那就没法提前将 num 的最新状态num 的以后状态 比拟,判断 状态是否变动

eagerState 的意义在于,在 某种状况 下,咱们能够在组件 render 前就提前计算出 最新状态 (这就是eagerState 的由来)。

这种状况下组件不须要 render 就能比拟 状态是否变动

那么是什么状况呢?

答案是:以后组件上 不存在更新 的时候。

当不存在 更新 时,本次更新就是组件的第一个 更新 。在只有一个 更新 的状况下是能确定 最新状态 的。

所以,eagerState的前提是:

以后组件不存在更新,那么首次触发状态更新时,就能立即计算出 最新状态 ,进而与 以后状态 比拟。

如果两者统一,则省去了后续 render 的过程。

这就是 eagerState 的逻辑。但遗憾的是,理论状况还要再简单一丢丢。

先让咱们看一个 看似不相干 的例子。

必要的 React 源码常识

对于如下组件:

function App() {const [num, updateNum] = useState(0);
  window.updateNum = updateNum;

  return <div>{num}</div>;
}

在控制台执行如下代码,能够扭转视图显示的 num 么?

window.updateNum(100)

答案是:能够。

因为 App 组件对应 fiber(保留组件相干信息的节点)曾经被作为 预设的参数 传递给 window.updateNum 了:

// updateNum 的实现相似这样
// 其中 fiber 就是 App 对应 fiber
const updateNum = dispatchSetState.bind(null, fiber, queue);

所以 updateNum 执行时是能获取 App 对应 fiber 的。

然而,一个组件理论有 2 个fiber,他们:

  • 一个保留 以后视图 对应的相干信息,被称为current fiber
  • 一个保留 接下来要变动的视图 对应的相干信息,被称为wip fiber

updateNum中被预设的是wip fiber

当组件触发更新后,会在组件对应的 2 个 fiber 上都 标记更新

当组件 render 时,useState会执行,计算出 新的状态 ,并把wip fiber 上的 更新标记 革除。

当视图实现渲染后,current fiberwip fiber 会替换地位(也就是说本次更新的 wip fiber 会变为下次更新的current fiber)。

回到例子

方才谈到,eagerState的前提是:以后组件不存在更新

具体来讲,是组件对应的 current fiberwip fiber都不存在更新。

回到咱们的例子:

第一次点击div, 打印:

App render 1
child render 

current fiberwip fiber 同时标记更新。

renderwip fiber更新标记 革除。

此时 current fiber 还存在 更新标记

实现渲染后,current fiberwip fiber 会替换地位。

变成:wip fiber存在更新,current fiber不存在更新。

所以第二次点击 div 时,因为 wip fiber 存在更新,没有命中eagerState,于是打印:

App render 1

renderwip fiber更新标记 革除。

此时两个 fiber 上都不存在 更新标记 。所以后续点击div 都会触发eagerState,组件不会render

总结

因为 React 外部各个局部间相互影响,导致 React 性能优化的后果有时让开发者蛊惑。

为什么没有听到多少人埋怨呢?因为性能优化只会反映在指标上,不会影响交互逻辑。

通过本文咱们发现,React性能优化并没有做到极致,因为存在两个 fibereagerState 策略并没有达到最现实的状态。

退出移动版