说说React内部的性能优化还没有达到极致?
时间:2023-03-22 14:41:59
科技观察
大家好,我是Kason。对于这种常见的交互步骤如下:点击一个按钮触发状态更新。组件渲染。查看渲染。您认为哪些步骤具有“性能优化空间”?答案是:1和2。对于“第1步”,如果状态更新前后没有变化,则可以跳过剩下的步骤。这种优化策略称为eagerState。对于“step2”,如果组件的后代节点没有状态变化,则可以跳过后代组件的渲染。这种优化策略称为bailout。看来eagerState的逻辑很简单,只需要比较“状态更新前后是否有变化”即可。然而,在实践中它是复杂的。本文通过理解eagerState的逻辑来回答一个问题:React的性能优化到极致了吗?一个奇怪的例子考虑了以下组件:functionApp(){const[num,updateNum]=useState(0);console.log("应用渲染",num);返回(updateNum(1)}>
);}functionChild(){console.log("childrender");return
child;}在线Demo地址[1]。第一次渲染,打印:Apprender0childrender第一次点击div,打印:Apprender1childrender第二次点击div,打印:Apprender1第三,第四……点击div,没有打印。在“第二次”点击中,打印Apprender1,不打印childrender。代表应用程序的后代组件没有呈现并命中了救助。“第三次和后续”点击不会打印任何内容,这意味着没有组件渲染并且点击了eagerState。那么问题来了,明明第一次和第二次点击都执行了updateNum(1),明明state没有变,为什么第二次没有点击eagerState呢?eagerState的触发条件首先我们要明白为什么叫eagerState(紧急状态)?通常,什么时候可以得到最新的状态?当组件呈现时。当组件呈现时,useState执行并返回最新状态。考虑以下代码:const[num,updateNum]=useState(0);useState执行后返回的num是最新状态。之所以在执行useState的时候可以计算出最新的状态,是因为状态是根据“一个或多个更新”来计算的。例如,在下面的点击事件中触发3次更新:constonClick=()=>{updateNum(100);updateNum(num=>num+1);updateNum(num=>num*2);num组件渲染时的最新状态应该是什么?首先,num变为100。100+1=101。101*2=202。因此,useState将返回202作为num的最新状态。实际情况会更复杂。更新有自己的优先级,所以在渲染之前无法确定“哪些更新会参与状态的计算”。因此,在这种情况下,组件必须渲染,并且必须执行useState以了解num的最新状态。那么就无法事先将num的最新状态与num的当前状态进行比较来判断“状态是否发生了变化”。eagerState的意义在于,在“特定情况下”,我们可以在组件渲染之前提前计算出最新的状态(这就是eagerState的由来)。在这种情况下,组件可以在不渲染的情况下比较“状态是否发生了变化”。那么是什么情况呢?答案是:当当前组件“没有更新”时。当没有更新时,本次更新为组件的第一次更新。当只有一个更新时,可以确定最新状态。因此,eagerState的前提是:当前组件没有更新,那么在第一次触发状态更新时,可以立即计算最新状态,然后与当前状态进行比较。如果两者一致,则省略后续的渲染过程。这就是eagerState的逻辑。但不幸的是,实际情况要复杂得多。我们先来看一个“看似无关紧要”的例子。以下组件所需的React源代码知识:functionApp(){const[num,updateNum]=useState(0);window.updateNum=updateNum;return
{num}
;}在控制台执行如下代码,能不能改变view显示的num?window.updateNum(100)答案是:是的。因为App组件对应的fiber(保存组件相关信息的节点)已经作为“预设参数”传给了window.updateNum://updateNum的实现类似这样//其中fiber是App对应的fiberconstupdateNum=dispatchSetState.bind(null,fiber,queue);所以在执行updateNum的时候,可以拿到App对应的fiber。然而,一个组件实际上有2条纤程,它们:一条保存了“当前视图”对应的相关信息,称为当前纤程。一个保存“下一个要改变的视图”对应的相关信息,称为一个wip纤程。updateNum里预设的是wipfiber。当组件触发更新时,会在组件对应的两条纤程上“标记更新”。当组件渲染时,useState会执行,计算新的状态,并清除wip纤程上的“更新标记”。当view渲染完成后,当前fiber和wipfiber会交换位置(也就是说本次更新的wipfiber会成为下一次更新的currentfiber)。回到刚刚提到的例子,eagerState的前提是:“当前组件没有更新”。具体来说,当前纤程和组件对应的wip纤程都没有更新。回到我们的例子:第一次点击div,打印:Apprender1childrendercurrentfiber和wipfibermarkupdate同时进行。渲染后,wip光纤的“更新标志”被清除。此时,当前纤程中还有一个“更新标志”。渲染完成后,当前光纤和wip光纤会交换位置。变成:wipfiber有更新,但是当前fiber没有更新。所以第二次点击div的时候,因为wipfiber更新了,eagerState没有命中,所以打印:Apprender1rendering之后,wipfiber的“updatemark”被清除了。此时,两根光纤上都没有“更新标记”。所以后续点击div将触发eagerState,组件将不会呈现。总结由于React各个部分之间的交互,React性能优化的结果有时会让开发人员感到困惑。为什么没有很多人抱怨?因为性能优化只会体现在指标上,不会影响交互逻辑。通过这篇文章,我们发现React的性能优化并没有做到极致。由于两条纤程的存在,eagerState策略还没有达到最优状态。参考[1]在线Demo地址:https://codesandbox.io/s/frosty-cerf-mg64o5?file=/src/App.js:188-200。