在react18之中,引入了transition的概念。而且有一个新的api和两个新的hooks
- startTransition
- useTransition
- useDeferredValue
场景应用:
比如通过输入框输入内容更新列表内容,对于用户来说,输入框输入之后立马反馈的优先级是高过于列表更新的优先级的。但是对于18之前的版本来说,更新没有分优先级,导致如果数据量大,列表更新慢,用户输入在输入框输入内容后很慢才能得到反馈,用户体验差。
而transition可以让列表更新的优先级降低,让用户输入的先处理,然后再处理列表更新。18之后开启了concurrent mode模式,更新也分了优先级的概念。
用法:
setData1(e)
startTransition(()=>{
setData2(e)
})
将需要延迟更新的任务放到startTransition去执行,这样里面的更新任务就会被赋予transition的优先级。
setTimeout
setTimeout也有类似于延迟更新的效果,比如
setData1(e)
setTimeout(()=>{
setData2(e)
}, 200)
- 跟startTransition相比,startTransition的本质是,将任务的优先级降低,所以当用户一致输入的时候,优先级较低的任务,不会阻碍输入框的回显。并且transition的时机,是比setTImeout更加合适的,他会在没有搞优先级任务的情况下,第一时间处理。而setTimeout的时间是人为控制的,不够准确。
- 第二则是,当渲染任务多的时候,setTImeout仍会阻碍搞优先级的任务,因为等延时时间一到,setTImeout里面的任务就会被执行,此时仍会阻止页面的交互。但是transition就不同了,在conCurrent mode下,react将该任务的优先级降低,等到浏览器空闲的时候才会执行。
- 但也不是说startTransition的任务永远不会影响主流程,因为在react中,每个任务都有过期时间,而过渡任务的过期时间比紧急任务长了很多而已,但是当该过渡任务迟迟得不到处理,并且已经过期的时候,react就会先处理这个过期的过渡任务,然后再继续调度。具体可以了解react的schedlue模块。
防抖
- 输入框的内容经常跟防抖节流打交道,但是其本质也是通过setTimeout,只不过控制了render的次数,而且防抖节流也需要人为的控制时间,而使用startTransition就不需要考虑这么多。
transition功能特性
transition顾名思义过度,状态更新的任务一般有两种,一种是紧急更新任务,比如用户交互行为,点击,输入等等。
而另一种则是过渡更新任务,比如上面列表的更新,这种任务的优先级低于紧急更新任务。
startTransition
startTransition(fn)
同步执行,但是fn里面的更新会被标记transition的优先级。也就是优先级更低了。
useTransition
startTransition只是用来降低优先级的,也就是过渡,但是过渡了多久我们需要一个状态来知道。useTransition就是用来做这个事情的。
const [isPending, startTransition] = useTransition()
startTransition(fn)
useTransition返回两个值,第一个就是是否正在过渡到控制变量,第二个就是跟startTransition用法一样的,用来降低任务优先级的函数。但任务处于过渡阶段的时候,isPending为true,告诉开发者,目前该任务正在过渡。
useDeferredValue
上述的startTransition和useTransition本质都是将一些任务的优先级降低。
而useDeferredValue可以让状态滞后更新。实现效果类似于transiton,当紧急的任务执行之后,再得到新的状态,而这个新的状态就是deferredValue。
跟useTransition的区别
同:内部实现一样,都是将任务标记为过渡更新任务。
别:
- useTransition是将处理一段逻辑,将任务变为过渡任务,useDeferredValue是将原值通过过渡任务后得到新的值,他是产生一个新的状态。
- 其次就是useDeferredValue的更新会更滞后于useTransition,因为内部是使用useEffect来调用startTranstion的。下面原理环节会讲到。
原理
startTransition
startTransition的源码实现
他的本质有点类似于17之前的事件更新,通过开关ReactCurrentBAtchConfig.transition
ReactCurrentBAtchConfig.transition默认值是null,再需要更新过渡任务的时候,将其置为{},这样startTransition里面的函数执行的时候,就会判断ReactCurrentBAtchConfig.transition以此来将其优先级降低。
useTransition
mount更新阶段,执行的函数是mountTransition,update阶段执行的函数是updateTransition
如图是mountTransition,本质很简单,就是通过调用mountState产生一个状态,然后通过startTransition绑定默认参数,调用moutnWorkInProgressHook创建hooks对象,跟其他hook链接,最后这个hooks.memoizedState保存的就是startTransition这个函数。
注意这里的startTransition跟上面的startTransition不一样,
如上,则是useTransition返回的第二个参数,本质就是先调用setPending(ture),然后切换transitiion上下文,让setPending(false)的更新也变成了过渡任务,这样setPending(false)和callback的任务优先级相同,他们会同时处理。
所以useTransition可以看成useState+startTransition
useDeferredValue
useDeferredValue本质也是调用了useState+useEffect,通过监听value值的改变,触发useEffect回调函数的更新,再去切换tranition上下文,更新setValue(value)
所以useDeferredValue可以看成:useState+useEffect+startTransition。
看看react如何通过判断transition上下文。
const [data, setData] = useState({})
startTransition(()=>setData({}))
一般startTransition里面函数,存在着触发更新的动作,比如useState的第二个参数setState,他们会创建update,更新优先级,开始调度。具体可以看hooks实现逻辑
所以重点看看产生update时候的优先级的处理,以setState为例子
useState的第二个参数,实际上是调用dispatchSetState
函数
requestUpdateLane就是用来获取当前update的优先级的。来看具体实现
第一个判断就是判断是否是同步模式下,也就是18之前的模式,他不支持优先级的概念。
其次就是判断是否是transition的上下文,判断方式也很简单粗暴,直接判断ReactCurrentBatchConfig.transition是否不等于null,是的话就表示处于transition的更新。
然后返回currentEventTransitionLane,也就是transition的优先级
这也就解释了通过切换transition上下文,产生的update的优先级并不一样。
然后处理这个update的时候
可以看到,setSe 产生的update的确走了这个逻辑,最后返回的优先级就是64。
然后将update插入hooks的update连表上,更新调用scheduleUpdateOnFiber开始新的一轮调度。
然后最后会执行ensureRootIsScheduled函数,该函数根据当前的优先级,调用schedule模块提供的schedule_callback函数调度任务。如下:
对于64的优先级,被分配到了normalPriority的优先级,
然后通过注册任务,开启调度。
自此,整个transition的流程就已经走通了。
总结:
-
transition过渡,就是将一些不紧要的任务降低其优先级,使其不阻塞高优先级的任务。
-
具体实现就是通过切换transition上下文,使产生的udpate获取更低的优先级,这样调度的时候过期时间就会更长,从而不会阻塞高优先级的任务。
-
文章通过学习掘金的《react进阶实践指南》作为笔记产出,文中图片部分来自《react进阶实践指南》