React 18 新特性之 startTransition

avatar
前端工程师 @公众号:ELab团队

前言

React 18 作为React下一个大版本,为React添加了一个新的机制 Concurrent rendering,大幅提高了React的性能,推出了时间切片和任务优先级的概念。下面是官方的描述: image.png

Concurrent features

React 18 将会是第一个加入对concurrent功能进行可选支持的版本:

  • startTransition: 可以让你的UI在一次花费高的状态转变中始终保持响应性
  • useDeferredValue: 可以让你延迟屏幕上不那么重要的部分的更新
  • <SuspenseList>: 可以让你控制loading状态指示器(比如转圈圈)的出现顺序
  • Streaming SSR with selective hydration: 让你的app可以更快地加载并可以进行交互

这次我们先学习一下startTransition这个全新的api,之后再对别的特性进行详细的分享。

startTransition

概述

React 18加入了一个全新的API startTransition,这个API相当牛,可以让我们的页面在大屏更新里保持响应。这个API通过标记某些更新为"transitions",来提高用户交互。可以说React可以让你在一次状态改变的过程中始终提供视觉上的回馈并且在这个过程中让浏览器能保持响应。

解决了什么问题

使APP始终感觉流畅和响应的并不容易。比如有时用户点击了一个按钮或者在输入框中输入,同时这些操作将会导致页面大量的更新,此时将会导致页面冻结或者挂起不动一会直到之前的更新任务完成为止。

我们先来来看一个Demo,这个Demo用的是React目前发布的版本17.0.2

React 17.0.2 Demo

可以发现每当用户输入时,我们更新input的value的同时用这个value去更新了一个有30000个item的list。然而这种大屏更新让页面无法及时响应,也让用户输入或者其他用户交互感觉很慢。

从理论上来说,这个问题是因为这里有两个不同的更新需要发生,第一个更新是一个紧急的更新,需要改变input field里的值,第二个更新则没相对没有那么紧急,展示用户输入后更新的列表结果。

// 紧急的更新:展示用户的输入 
setInputValue(e.target.value); 
 
// 非紧急的更新: 展示结果 
setContent(e.target.value); 

用户通常会期待第一个更新更加及时,第二个更新则可以稍微延迟一会。工作中,我们经常通过使用一些方法来人为地delay一些更新,比如说debounce。然而debounce也只能创造一种次优的用户体验,原因是一旦render开始便不能被打断,而concurrent模式则打破了这个限制让render过程可被打断,当用户按下一个键时,React不用block住浏览器去更新输入框,反之可以让浏览器去更新input然后再接着render list。所以concurrent模式使React不再需要通过依赖debounce去人为地delay work来避免卡顿了。 ​

在React 18之前,所有的更新没有优先级之分,都是紧急的,这意味着上面的两种状态更新会被同时render,并且仍然会block住用户从他们的交互中获得反馈直到所有的东西都render好。

startTransition是如何解决这个问题的

这个新的API startTransition通过给予我们能力去标记一个更新为"transitions"来解决了这个问题: 我们再来看一个Demo,同样是之前的那个场景,但是这次我们使用的是React还未正式发布的18版本。

React 18.0.0-alpha Demo

// 紧急的更新:展示用户的输入 
setInputValue(e.target.value); 
 
// 将非紧急的更新标记为"transitions" 
startTransition(() => { 
    setContent(e.target.value); 
}); 

被包裹在startTransition里的更新会被作为非紧急更新来处理,并且会被其它的紧急更新打断。如果transition被用户打断了,React将停止还没进行完的rendering工作,并且render这次更加紧急的更新。

startTransition的实现原理是什么

上面介绍了这么多,接下来我们通过源码看下startTransition的底层是如何实现上述功能的,这里我看的是react库里master(568dc3532e25b30eee5072de08503b1bbc4f065d)分支上最新的代码,目前还在不断更新中。

ReactDOM.createRoot:开启Concurrent模式

首先要开启Concurrent模式,这里不能为某个子树单独启用concurrent模式,而应该在ReactDOM.render()里启用它,然后使用ReactDOM.createRoot()来替换ReactDOM.render(),这样会在整个结构树里启用concurrent模式

startTransition

Scope: 对应了demo里我们传给startTransition的函数:

() => { setContent(e.target.value) }

ReactCurrentBatchConfig: 1.png

一个全局变量,用来跟踪当前批的配置,可以看到里面有一个transition属性,属性的初始值为0 再回到startTransition的代码:

  1. prevTransition = ReactCurrentBatchConfig.transition;
  2. ReactCurrentBatchConfig.transition = 1;
  3. scope();
  4. ReactCurrentBatchConfig.transition = prevTransition;

总的来说就是在执行更新前将ReactCurrentBatchConfig里的transition属性赋值为1,标记这次Update为"transition",更新结束后再将transition属性赋为初始值0

dispatchAction

到这里我们开始执行传入的scope函数了,而这个函数里包裹了我们的更新,即setState,实际上就是调用了dispatchAction(更新的入口) 2.png

这里有一个很重要的函数requestUpdateLane,也就是奇迹开始的地方,这个函数将会返回给我们update的优先级(还记得我们前面所说的紧急与非紧急的更新吗),那么什么是优先级呢?这里涉及到React Lane模型的概念,这里有一篇不错的文章大家可以去看下React Lane模型,这里我就简单介绍一下。

ReactFiberLane

想象一条这样的赛车赛道,是不是越靠近内圈的赛道越短,越靠近外圈的赛道越长,相邻的赛道则长度差不多,React运用了同样的概念,通过31位的二进制来表示31条赛道,其中位数越小的赛道优先级越高。

这里我们所说的赛道即为Lane,一些赛道组合在一起则为Lanes,下面看下源码:

可以看到SyncLane的优先级是最高的,而TransitionLane则有16条TransitionLanes则包含了所有的TransitionLane

requestUpdateLane

首先我们可以看到如果不是concurrentMode的话将会直接返回SyncLane

requestCurrentTransition

这里会返回ReactCurrentBatchConfig里的transition属性,还记得上文中在介绍startTransition时,每次调用startTransition会将1赋给ReactCurrentBacthConfig.transition,所以此时我们返回的值则为1。 再回到requestUpdateLane的源码里:

isTransition = requestCurrentTransition() !== NoTransition

NoTransition即为0,requestCurrentTransition返回为1,isTransition为true,进入if block里的逻辑: 1.png 这里React做了个设计,同一个事件触发的多个标记为"transition"的update会被安排到同一条Lane即具有相同的优先级

claimNextTransitionLane

这个函数则会返回当前事件触发的标记为"transition"的update的优先级。

export function claimNextTransitionLane(): Lane { 
  const lane = nextTransitionLane;  
  // 初始值为TransitionLane1: 0b0000000000000000000000001000000 
  nextTransitionLane <<= 1;  
  /*将字节向左移一位 比如: 0b0000000000000000000000001000000 =>  
                        0b0000000000000000000000010000000 
    等于从TransitionLane1 到了 TransitionLane2*/ 
  if ((nextTransitionLane & TransitionLanes) === 0) { 
  // TransitionLanes用完了(16条)则从头开始排,优先级TransitionLane1 
    nextTransitionLane = TransitionLane1; 
  } 
  return lane; 
} 

回顾之前的Lane模型,一共有16条TransitionLane,我们自上而下给每一个transition update安排一条Lane,直到16条TransitionLane用完,我们再从头开始给下一个更新安排第一条Lane即TransitionLane1可以想象成在一个商场里,我们从顶楼开始找停车位,有位置就停,到一楼还没有找到位置则再回楼顶找

getEventPriority: 判定其他更新的优先级

现在我们知道了通过startTransion()包裹的更新会被给予一个“不那么紧急”的优先级TransitionLane,那么其他更新的优先级又是如何判定的呢?比如说我们demo里的输入事件。

React通过触发更新的事件类型进行了优先级的判断:

可以看到输入事件的优先是最高的,这也是为什么Demo里输入的数字会优先渲染出来。

结语

现在我们拿到了这次更新的优先级TransitionLane,也就是我们Demo里所说的“不那么紧急的优先级”,而用户的输入事件则拿到了更高的优先级DiscreteEventPriority(SyncLane),也就是“紧急的优先级”,接下来就进入React内部的调度更新流程了,这里有一个很重要的概念Scheduler,也是React 18实现按优先级更新和时间切片的核心,这里先把目前整理的流程图摆上(还需完善),后续再做分享。

2.png

❤️ 谢谢支持

以上便是本次分享的全部内容,希望对你有所帮助^_^

喜欢的话别忘了 分享、点赞、收藏 三连哦~。

欢迎关注公众号 ELab团队 收货大厂一手好文章~

我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。

我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计与营销等内容。

字节跳动校/社招内推码: JD345JU

投递链接: jobs.toutiao.com/s/RfxWn66