当前位置: 首页 > Web前端 > HTML

React内部的性能优化还没有达到极致?

时间:2023-03-28 01:52:10 HTML

大家好,我是Kason。对于以下常见的交互步骤:点击按钮,触发state更新组件render视图渲染,您认为哪些步骤有性能优化的空间?答案是:1和2。对于第1步,如果状态更新前后没有变化,则可以跳过剩下的步骤。这种优化策略称为eagerState。对于步骤2,如果组件的后代节点没有状态变化,则可以跳过后代组件的渲染。这种优化策略称为bailout。看来eagerState的逻辑很简单,只需要比较state更新前后有没有变化就可以了。然而,在实践中它是复杂的。本文通过理解eagerState的逻辑来回答一个问题:React的性能优化到极致了吗?欢迎加入人类优质前端框架群,举个奇怪的例子考虑以下组件:functionApp(){const[num,updateNum]=useState(0);console.log("应用渲染",num);返回(updateNum(1)}>

);}functionChild(){console.log("childrender");returnchild;}在线Demo地址firstrender,print:Apprender0childrenderfirstclickondiv,print:Apprender1childrendersecondclickondiv,print:Apprender1third,fourth...单击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变为100100+1=101101*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个fiber,它们:一个保存了当前view对应的相关信息,称为currentfiber,另一个保存了下一个要改变的view对应的相关信息,称为wippreset在fiberupdateNum中是wipfiber。当组件触发更新时,会在组件对应的两条纤程上标记更新。当组件渲染时,useState将执行,计算新状态,并清除wip纤程上的更新标志。当view渲染完成后,currentfiber和wipfiber会交换位置(也就是说更新后的wipfiber会成为下一次更新的currentfiber)。回到刚刚提到的例子,eagerState的前提是:当前组件没有更新。具体来说,当前纤程和组件对应的wip纤程都没有更新。回到我们的例子:第一次点击div,打印:Apprender1childrendercurrentfiber和wipfibermarkupdate同时进行。wip光纤的更新标志在渲染后被清除。这个时候,当前的fiber还是有update标记的。渲染完成后,当前光纤和wip光纤会交换位置。变成:wipfiber有更新,但是当前fiber没有更新。所以第二次点击div时,由于wipfiber的更新,eagerState没有命中,所以打印:Apprender1Theupdatemarkofwipfiberisclearedafterrendering。此时,两根光纤上都没有更新标记。所以后续点击div将触发eagerState,组件将不会呈现。总结由于React各个部分之间的交互,React性能优化的结果有时会让开发人员感到困惑。为什么没有很多人抱怨?因为性能优化只会体现在指标上,不会影响交互逻辑。通过这篇文章,我们发现React的性能优化并没有做到极致。由于两条纤程的存在,eagerState策略还没有达到最优状态。