React18 并发特性,还能玩出花来。用useTransition实现防抖!

1,764 阅读3分钟

在前端的世界,为了减低后端的服务器压力。

在某些场景下,我们往往需要对也许操作进行防抖字流操作。从而降低都服务端的请求。网上关于这方便的内容比较多了,大家可以自行查阅防抖字流的概念,与实现。

实现上面两个功能并是什么难点,有兴趣的同学可以去看看ahooks的实现。今天主要介绍的React18中基于新的并发特性,React原生实现了防抖的功能。

useTransition

前端时间React18的消息满天飞,大家都知道useTransition是一个新增原生Hooks,官方定义用于并发处理(较低优先级执行一些更想)。

我们举一个简单的例子,以下的场景

function App() {
    const [ctn,updateCtn] = useState('')
    const [num,updateNum] = useState(0)
    const [isPending, startTransition] = useTransition();
    
    return (
        <div>
            <input value={ctn} onChange={({taget:{value}})=>{
                updateCtn(value)
                // 标记非紧急更新来处理,差不多后端并发一样变异步了,主线程不执行。但这里概念会变成等待更新
                startTransition(()=>updateNum(num+1))
            }}/>
            
            <BusyChild num={num}/>

        </div>
    )
    
} 

// 由于程序相对简单,需要做render的时间延迟,这样我们就更好的看到效果

const BusyChild = React.memo(({num}:{num:number})=>{
    const cur = performance.now();
      // 增加render的耗时 时间越大,卡顿效果越明显
    while (performance.now() - cur < 100) {}
    
    return <div>{num}</div>;
})

这个时候我们可以看到的结果如下,updateNum执行后,BusyChild的操作会延迟进行操作。

image.png

是如何实现的?

在React18中添加了一种类似优先级渲染的机制。不同触发拥有不同的优先级。就比如刚刚的场景,我们使用useTransition设置了紧急更新处理。

在React18中怎么表示优先级呢?

const SyncLane =    0b0000000000000000000000000000001;
const DefaultLane = 0b0000000000000000000000000010000;

数值越小优先级越大,即SyncLane < DefaultLane。

那么React每次更新是不是选择一个优先级,然后执行所有组件中「这个优先级对应的更新」呢?

不是。如果每次更新只能选择一个优先级,那灵活性就太差了。

所以实际情况是:每次更新,React会选择一到多个lane组成一个批次,然后执行所有组件中「包含在这个批次中的lane对应的更新」

这种组成批次的lane被称为lanes。

比如,如下代码将SyncLane与DefaultLane合成lanes:

// 用“按位或”操作合并lane
const lanes = SyncLane | DefaultLane;

entangle机制

可以看到,lane机制本质上就是各种位运算,可以设计的很灵活。

在此基础上,有一套被称为entangle(纠缠)的机制。

entangle指一种lane之间的关系,如果laneA与laneB纠缠,那么某次更新React选择了laneA,则必须带上laneB。

也就是说laneA与laneB纠缠在一块,同生共死了。

除此之外,如果laneA与laneC纠缠,此时laneC与laneB纠缠,那么laneA也会与laneB纠缠。

那么entangle机制与useTransition有什么关系呢?

被startTransition包裹的回调中触发的更新,优先级为TransitionLanes中的一个。

TransitionLanes中包括16个lane,分别是TransitionLane1到TransitionLane16:

位于 react/packages/react-reconciler/src/ReactFiberLane.new.js

image.png

而transition相关lane会发生纠缠。

在我们的Demo中,每次onChange执行,都会创建两个更新:

onChange={({target: {value}}) => {
  updateCtn(value);
  startTransition(() => updateNum(num + 1))
}

其中:

  • updateCtn(value)由于在onChange中触发,优先级为SyncLane。
  • updateNum(num + 1)由于在startTransition中触发,优先级为TransitionLanes中的某一个。

当在输入框中反复输入文字时,以上过程会反复执行,区别是:

  • SyncLane由于是最高优先级,会被执行,所以我们会看到输入框中内容变化。
  • TransitionLanes相关lane优先级比SyncLane低,暂时不会执行,同时他们会产生纠缠。

为了防止某次更新由于优先级过低,一直无法执行,React有个「过期机制」:每个更新都有个过期时间,如果在过期时间内都没有执行,那么他就会过期。

过期后的更新会同步执行(也就是说他的优先级变得和SyncLane一样)。

在我们的例子中,startTransition(() => updateNum(num + 1))会产生很多纠缠在一块的TransitionLanes相关lane。

过了一段时间,其中某个lane过期了,于是他优先级提高到和SyncLane一样,立刻执行。

又由于这个lane与其他TransitionLanes相关lane纠缠在一起,所以他们会被一起执行。

这就表现为:在输入框一直输入内容,但是num在视图中显示的数字过了会儿才变化。

总结

今天我们聊了useTransition内部的一些实现,涉及到:

  • lane模型。
  • entangle机制。
  • 更新过期机制。

最有意思的是,由于不同电脑性能不同,浏览器帧率会变动,所以在不同电脑中React会动态调节防抖的效果。

这就相当于不需要你手动设置debounce的时间参数,React会根据电脑性能动态调整。