react-关于react为什么要从ExpirationTime切换到lane的一次考古

作者&投稿:线武 (若有异议请与网页底部的电邮联系)
~ react-关于react为什么要从ExpirationTime切换到lane优先级的一次考古

最近本来打算出一篇关于lane优先级的文章,但是却卡在了react为什么要将ExpirationTime优先级更换为lane这部分。我在网上找了很多搜索工作,但是很少找到关于这个话题的细节讨论。官方关于这个改动也只有零星的issue可以参考。目前大部分的文章中谈到lane优先级的出现是因为高优先级IO任务会阻塞了低优先级的CPU任务并且还附带了相关的实例代码。但是我在复现这个问题的过程中发现切换为lane的原因肯能并非如此。这篇文章将会以React-16.12.0来证明这个结论。

从一个issue说起

一切还要从react的一个issue谈起。有位老哥在react的issue中提出了一个问题:Somequestionsaboutlanes。大概意思是你们这个lane优先级看起来excited,但是你们有什么例子可以用来说明吗?这时候react内部的一位大佬就贴上了codesandbox上的示例代码:beforelan。可能是由于版本的问题,这段代码以及无法正常运行。因为里面涉及到了Suspense,Promise,useTransaction所以也被很多文章当作高优先级IO任务阻塞低优先级CPU任务的一个示例。我现在直接将这段代码贴出来:

import?React,?{????Suspense,????useState,????useEffect,????useTransition,????Fragment??}?from?"react";??const?sleep?=?(durationMs)?=>????new?Promise((resolve)?=>?setTimeout(()?=>?resolve(),?durationMs));??const?wrapPromise?=?(promise)?=>?{????let?result;????promise.then(??????(value)?=>?{????????result?=?{?type:?"success",?value?};??????},??????(value)?=>?{????????result?=?{?type:?"error",?value?};??????}????);????return?{??????read()?{????????if?(result?===?undefined)?{??????????throw?promise;????????}????????if?(result.type?===?"error")?{??????????throw?result.value;????????}????????return?result.value;??????}????};??};??const?createResource?=?(durationMs)?=>?{????return?wrapPromise(sleep(durationMs).then(()?=>?"FETCHED?RESULT"));??};??function?Sub({?count?})?{????const?[resource,?setResource]?=?useState(undefined);????const?[startTransition,?isPending]?=?useTransition({?timeoutMs:?4000?});????console.info(`resource?=?${resource}`)????return?(??????<div>????????<button??????????onClick={()?=>?{????????????startTransition(()?=>?{????????????????console.info("start?resource")????????????????setResource(createResource(10000));????????????????console.info("end?resource")????????????});????????????//?setResource(createResource(40000))??????????}}????????>??????????CLICK?ME????????</button>????????<pre>{JSON.stringify({?count,?isPending},?null,?2)}</pre>????????{resource?===?undefined???"Initial?state"?:?resource.read()}??????</div>????);??};??function?EmptyWrap(props){????return?(??????props.children????)??}??function?LaneTest(props){????const?[s,?setS]?=?useState(0);????useEffect(()?=>?{??????const?t?=?setInterval(()?=>?{????????console.info("===================================?start?=============================")????????setS((x)?=>?x?>?10???x:??x?+?1);??????},?1000);??????return?()?=>?{????????clearInterval(t);??????};????},?[]);????const?a?=?(??????<>????????<button?onClick={()?=>?{??????????console.info("==============================?start?==============================")??????????setS(s?+?1)????????}}>+1</button>??????????<Suspense?fallback={<div>loading...</div>}>????????????<Sub?count={s}?/>??????????</Suspense>????????<span>{s}</span>??????</>??????)????return?a;??};????export?default?LaneTest;

实际上代码非常简单,使用了Promise以及Suspense模拟一次IO请求。关于Suspense我这里大概提一下原理,当我们在Suspense中raise一个类型为Promise的异常时react会捕获这个Promise并将Suspense子组件切换为fallback中的内容,等这个promiseresolve时再渲染真正的子组件内容。总的来说,就是将query-pendingstate-complete这三个状态管理通用化了。具体流程就是:发起请求-Suspense展示fallback-请求完成展示真实子组件。

但是有时如果请求很快完成就会出现fallback一闪而过的状态造成用户体验不佳。这个问题react也考虑到了,推出了useTransition这个hook,其中可以传入一个参数来控制fallback,使其可以延迟展示。假设将其设置为{timeoutMs:1000}则表示发起请求的1s内不展示fallback界面,如果请求1s内完成了就直接显示真实的子页面,否则超过1s再开始展示fallback。避免了fallback闪屏的现象。

这段代码的运行结果如下:

当点击按钮后,请求已经发出,但此时并没有立即显示fallback。同时count也停止增加,等到useTransitiontimeout之后才显示fallback内容loading...。这里有一点符合我们的预期,但也有一旦不符合我们的预期:

符合预期:fallback等到useTransitiontimeout之后才显示

不符合预期:fallback虽然没显示,但是count也不再增加,看起来像整个界面被卡住了

但是真的是高优先级的IO任务阻塞了低优先级的CPU任务吗?

谁阻塞了谁

从上面的现象可以看出来,我们的渲染任务的确被阻塞了。但是现在有了新的问题:

到底是被谁阻塞的?

阻塞到哪个步骤了?

阻塞的时候有真正执行的任务吗?

让我们先来做一个实验把startTransaction去掉,保留Suspense。我们会发现,去掉startTransaction后点击按钮会立即显示loading...。但是count却不会出现卡住的现象。我们在去掉startTransaction保留了Suspense以及模拟的IO任务的情况下发现页面并没有被。因此到这里我们得出了第一个结论:

页面卡住,并不是因为高优先级IO任务的阻塞

此时第一个问题还没有真正的回答,我们现在知道阻塞渲染另有其人,但是到底是什么呢?经过我长时间对源码的调试以及阅读,阻塞渲染的原因实际上是:

同时使用Suspense以及startTransaction

这哥俩可以说是缺一不可,但凡你换掉其中的一个页面都不会被阻塞。但是熟话说得好:我咋知道你是不是乱编的,justshowmethecode。接下来我们从源码的角度来分析整个问题。

首先找ReactFiberWorkLoop.js中的finishConcurrentRender这个函数。这个函数的作用就是根据exitStatus来做各种操作,然后后续会渲染真实dom。注意这里已经到最后的diff提交阶段了。如果我们同时使用了Suspense以及startTransaction那么existStatus就为RootSuspendedWithDelay,并且此时代码会计算出一个msUntilTimeout,表示离startTransactiontimeout的时间,如果这个时间还大于10,react就会说这timeout还早着呢,到了再说把,这把先不渲染了:

?//?Don't?bother?with?a?very?short?suspense?time.????????if?(msUntilTimeout?>?10)?{??????????//?The?render?is?suspended,?it?hasn't?timed?out,?and?there's?no??????????//?lower?priority?work?to?do.?Instead?of?committing?the?fallback??????????//?immediately,?wait?for?more?data?to?arrive.??????????root.timeoutHandle?=?scheduleTimeout(?//?调度一个?timeout?后执行的任务????????????commitRoot.bind(null,?root),????????????msUntilTimeout,??????????);??????????console.info("commit?delay")??????????break;????????}

可以看出调度了一个msUntilTimeout的回调就直接break了,没有调用后续的commitRoot,自然也没有任何真正的dom改变发生。而此时其他需要提交的diff瑟瑟发抖,怎么的就你要延迟提交我们就得跟着遭殃啊?没错react就是这么任性,就算同一个任务中还有其他的diff需要提交react也一并延迟了。这也就是我们点击按钮后为什么count会停止增加的原因。渲染发生,当然也就看不到最新的count。但是实际上状态的计算并没有被阻塞,只是complete阶段没有提交diff。到这里,开头提出的3个问题我们已经可以回答了:

到底是被谁阻塞的:被Suspense和startTransaction双剑合并阻塞的,当然我认为称为跳过更合适

阻塞到哪个步骤了:阻塞(跳过)了completework

阻塞的时候在做什么:该干啥干啥,该diff还diff,只不过最后不给渲染

这三个问题回答完了,但是这个问题的本质我们还没有找到。什么情况导致了渲染count的时候existStatus被设置为RootSuspendedWithDelay呢?我们继续搜索会发现,在ReactFiberCompleteWork.js的completeWork中当组件类型为SuspenseComponent时且满足条件current.memoizedState===null&&workInProgress.memoizedState!==null时就会调用renderDidSuspendDelayIfPossible这个函数,将existStatus设置为RootSuspendedWithDelay:

if?(????workInProgressRootExitStatus?===?RootIncomplete?||????workInProgressRootExitStatus?===?RootSuspended??)?{????console.info("set?workInProgressRootExitStatus?=?RootSuspendedWithDelay")????workInProgressRootExitStatus?=?RootSuspendedWithDelay;??}

那么为什么我们点击了'CLICKME'按钮之后这个wip.memoizedState就不为空了呢?在ReactFiberBeignWork.js的updateSuspenseComponent函数中如果满足条件constdidSuspend=(workInProgress.effectTag&DidCapture)!==NoEffect,最终会进入到:

if?(nextDidTimeout)?{????????workInProgress.memoizedState?=?SUSPENDED_MARKER;????????workInProgress.child?=?primaryChildFragment;????????return?fallbackChildFragment;

这个分支,在这里会将workInProgress.memoizedState=SUSPENDED_MARKER。这也就是为什么在completeWork中workInProgress.memoizedState不为空的原因。现在问题又变成了constdidSuspend=(workInProgress.effectTag&DidCapture)!==NoEffect这个条件什么时候会满足?在之前提到过,如果我们要进行io请求,首先要抛出一个Promise,并且会由react去捕获。如果捕获到了就会给就近的Suspense打上这个effectTag。也就是说,一旦当我们抛出Promise,这个effectTag就会打上,也就会导致后面跳过渲染。最后回到自己编写的代码,什么情况会抛出Promise:

function?Sub({?count?})?{????const?[resource,?setResource]?=?useState(undefined);????const?[startTransition,?isPending]?=?useTransition({?timeoutMs:?4000?});????console.info(`resource?=?${resource}`)????return?(??????<div>????????<button??????????onClick={()?=>?{????????????startTransition(()?=>?{????????????????console.info("start?resource")????????????????setResource(createResource(10000));????????????????console.info("end?resource")????????????});????????????//?setResource(createResource(40000))??????????}}????????>??????????CLICK?ME????????</button>????????<pre>{JSON.stringify({?count,?isPending},?null,?2)}</pre>???????//?当?resource?!==?undefined,时会调用?resource.read(),如果此时的?promise?还没有?resolve?就会抛出?promise????????{resource?===?undefined???"Initial?state"?:?resource.read()}??????</div>????);??};

也就是说只要resource!==undefined并且这个promise还没有被resolve就会被抛出。而resource初始化为undefined。当我们点击onclick后会调用:

onClick={()?=>?{????????????startTransition(()?=>?{????????????????console.info("start?resource")????????????????setResource(createResource(10000));????????????????console.info("end?resource")????????????});????????????//?setResource(createResource(40000))??????????}}

也就是只要这个setResource(createResource(10000))被执行了,就会抛出promise,也就会导致最后的跳过渲染问题。那么现在重点来了,如果两个setState的优先级相同,react会复用同一个任务来处理,此时我们代码里面有两个setState,分别为:

用于增加count的setS((x)=>x>10?x:?x+1),每秒增加一个

用于对resource进行赋值的setResource(createResource(10000))

那么有没有可能这两个setState的优先级相同,导致在进行count的渲染和Suspense子节点渲染在同一个任务呢?没错,我们终于找到了真正的答案!!!我们知道如果是用户事件触发的setState,react会根据事件的类型来选择不同的优先级,如果是其他地方的setState,例如setTimeout,react会采用一个默认优先级。而useTransition也有相同的控制优先级的操作,它会根据timeout的时长来计算优先级,时长越长,优先级越低。而我们选取的{timeoutMs:4000}正好计算出来的优先级与默认优先级相同,导致这两个setState在同一个task中处理。因此我们最终的结论是:

由于计算countstate以及setResouce的优先级一致,导致渲染count的任务与处理Suspense的任务实际上是同一个任务。此时Suspense需要延迟渲染,导致了渲染count的任务也延迟进行。

到这里,我们基本找出了页面被阻塞的原因。虽然说和IO有一点关系,但的确不是高优先级的IO任务阻塞了低优先级的CPU任务,因为正因为两个任务的优先级相同才导致了阻塞或者是跳过的问题。

lane可以解决这个问题吗?

在我们知道了问题的原因后,当然也想知道如何解决这个问题。实际上答案非常简单,既然原因是useTransition的优先级和其他任务优先级一致,那么我们只需要降低useTransition计算出的优先级,让其小于任何其他任务的优先级即可。这个想法也非常好验证:由于用户点击是一个优先级非常高的事件,我们在点击CLICKME按钮触发setResource后继续点击+1按钮增加count这时可以看到count的渲染完全不受影响:

并且fallback渲染也和我们预期一致。原因也很好解释:

由于click事件产生的渲染优先级非常高,导致计算setResourcestate计算被直接跳过。因此在渲染的任务中,Sub组件的resource为undefined,如果resource为undefined就不会抛出Promise异常,自然也没有延迟渲染fallback的行为了,此时count可以正常更新。

至于如果不清楚为什么setResource会被跳过,可以参考我的另一篇文章:updateQueue原理。

那么说了这么多,lane可以解决这个问题吗?当然可以,lane优先级是由二进制位来控制的,并且为1的那一位越靠近右边优先级就越高,那么只需要在lane里面把useTransition的优先级设置得比其他优先级都靠左,这个问题就完美解决了。那么react真的是这么做的吗?我只能说react的开发者肯定比我聪明,让我们直接看react18.01中对优先级定义的源代码:

export?const?NoLanes:?Lanes?=?/*????????????????????????*/?0b0000000000000000000000000000000;export?const?NoLane:?Lane?=?/*??????????????????????????*/?0b0000000000000000000000000000000;export?const?SyncLane:?Lane?=?/*????????????????????????*/?0b0000000000000000000000000000001;export?const?InputContinuousHydrationLane:?Lane?=?/*????*/?0b0000000000000000000000000000010;export?const?InputContinuousLane:?Lane?=?/*?????????????*/?0b000


翻译一段话,英译汉
我对你学校有那么大感到非常吃惊,在这里没有这么大的学校。并且在大学校里评分的主修课大约有三或五个,每科平均有三十个同学。这里,在巴西,我们每周去五天学校,从周一到周五,然后我们也在周末放假。早上7:45学校开始上课,12:15下课。我们通常学葡萄牙语,英语(我英语不好,这就是我上英语课...

六年级上册PEP英语单词分类 好的加100分
that’s = that is who’s = who is what’s = what is they’re = they are isn’t = is not aren’t = are not can’t = cannot don’t = do not doesn’t = does not let’s = let us 参考资料: http:\/\/www.eact.com.cn\/bbs\/viewthread.php?tid=2384&page=1 已赞过 已踩过<...

潢川县18487847062: 为什么我们需要使用React提供的Children API而不是JavaScript的map? -
呼军莱斯: 这个是react最新版api,也就是0.14版本做出的改变.主要是为了使React能在更多的不同环境下更快、更容易构建.于是把react分成了react和react-dom两个部分.这样就为web版的react和移动端的React Native共享组件铺平了道路.也就是说...

潢川县18487847062: 为什么React.js这么火 -
呼军莱斯: Angular和React不属于同一类的东西,Angular是一个框架,而React更多是负责UI视图部分,大家普遍认为React是MVC中的V.那么到底为什么React.js一下子就火起来了呢?个人觉得可能主要是因为以下几个因素所导致的:单向数据绑定 就在...

潢川县18487847062: React.js 究竟解决了什么问题 -
呼军莱斯: React 通常和其他的 JavaScript 框架同时被提及,但是说“React 对比 Angular”却讲不通,因为它们之间是不可比较的.Angular 是一个完整的框架(包括一个 view 层),React 却并不是.这也是 React 很难于理解的原因,它虽然抽离自一个具备完整框架的生态系统中,但仅仅是一个 view 层.React 提供了模板语法以及一些函数钩子用于基本的 HTML 渲染.这就是 React 全部的输出——HTML.你把 HTML / JavaScript 合到一起,被称为“组件”,允许把它们自己内部的状态存到内存中(比如在一个选项卡中哪个被选中),不过最后你只是吐出 HTML.

潢川县18487847062: 如何正确,客观地评价 React -
呼军莱斯: 1、react目前比不上angular流行,主要就是中文资料少的可怜,学习源码也少,严重阻碍了很多看不懂英文API的学习者;2、单纯学习react并没什么卵用,都是要和其他框架模式整合开发,这就需要学习者同时掌握很多高级的开发知识;3、如...

潢川县18487847062: react - native是用什么语言开发的 -
呼军莱斯: 1,React Js的目的是为了使前端的V层更具组件化,能更好的复用,它能够使用简单的html标签创建更多的自定义组件标签,内部绑定事件,同时可以让你从操作dom中解脱出来,只需要操作数据就会改变相应的dom.2,React Native的目的是希望我们能够使用前端的技术栈就可以创建出能够在不同平台运行的一个框架.可以创建出在移动端运行的app,但是性能可能比原声app差一点.

潢川县18487847062: React 和 Angular 各有什么优缺点,各自又适合什么开发场景 -
呼军莱斯: Angular.js 首先Angular的背后是Google(难道这就是官网被墙的原因?),所以社区基础是不用担心的,整个生态也已经是非常的完整了,从最基本的Tutorial到StackOverflow的问题数到框架本身的剖析都有非常非常多,所以从这个角度看起来...

潢川县18487847062: React数据获取为什么一定要在componentDidMount里面调用 -
呼军莱斯: 这与React组件的生命周期有关,组件挂载时有关的生命周期有以下几个:constructor() componentWillMount() render() componentDidMount() 上面这些方法的调用是有次序的,由上而下,也就是当说如果你要获取外部数据并加载到组件上,...

潢川县18487847062: 为什么我弃用 Angular,转向 React -
呼军莱斯: 这方面文章不少,自己找几个看看便知.抄一段:React速度很快与其它框架相比,React采取了一种特立独行的操作DOM的方式.它并不直接对DOM进行操作.它引入了一个叫做虚拟DOM的概念...

潢川县18487847062: 帮我理解一个很简单的词react -
呼军莱斯: re是前缀,act是行动的意思,加上前缀就是反应的意思. reaction是react的名词,reactant是形容词.还有就是你在记单词的时候,用联想记忆法比较好,记其中的一个动词,就可以联想到其他的词性.还有些单词是可以加上前缀或是后缀,建议多看看语法书.希望能帮到楼主!

潢川县18487847062: react是什么意思
呼军莱斯: react [ri5Akt] vi. 起反应, 起作用, 反抗, 起反作用 hypocrisy [hi5pCkrEsi] n. 伪善

本站内容来自于网友发表,不代表本站立场,仅表示其个人看法,不对其真实性、正确性、有效性作任何的担保
相关事宜请发邮件给我们
© 星空见康网