React如何原生实现防抖?

开发 前端
ouseTransition是一个新增的原生Hook,用于「以较低优先级执行一些更新」。在我们的Demo中有ctn与num两个状态,其中ctn与输入框的内容受控。

大家好,我卡颂。

作为前端,想必你对防抖(debounce)、节流(throttle)这两个概念不陌生。

在React18中,基于新的并发特性,React原生实现了防抖的功能。

今天我们来聊聊这是如何实现的。

useTransition Dem

ouseTransition是一个新增的原生Hook,用于「以较低优先级执行一些更新」。

在我们的Demo中有ctn与num两个状态,其中ctn与输入框的内容受控。

当触发输入框onChange事件时,会同时触发ctn与num状态变化。其中「触发num状态变化的方法」(即updateNum)被包裹在startTransition中:

function App() {
const [ctn, updateCtn] = useState('');
const [num, updateNum] = useState(0);
const [isPending, startTransition] = useTransition();
return (
<div >
<input value={ctn} onChange={({target: {value}}) => {
updateCtn(value);
startTransition(() => updateNum(num + 1))
}}/>
<BusyChild num={num}/>
</div>
);
}

num会作为props传递给BusyChild组件。在BusyChild中通过while循环人为增加组件render所消耗的时间:

const BusyChild = React.memo(({num}: {num: number}) => {
const cur = performance.now();
// 增加render的耗时
while (performance.now() - cur < 300) {}
return <div>{num}</div>;
})

所以,在输入框输入内容时能明显感到卡顿。

在线示例地址[1]。

按理说,onChange中会同时触发ctn与num的状态变化,他们在视图中的显示应该是同步的。

然而实际上,输入框连续输入一段文字(即ctn的状态变化连续展示在视图中)后,num才会变化一次。

如下图,初始时输入框没有内容,num为0:

输入框输入很长一段文字后,num才变为1:

这种效果就像:被startTransition包裹的更新都有「防抖」的效果一样。

这是如何实现的呢?

什么是lane

在React18中有一套「更新优先级机制」,不同地方触发的更新拥有不同优先级。优先级的定义依据是符合用户感知的,比如:

  • 用户不希望输入框输入文字会有卡顿,所以onChange事件中触发的更新是同步优先级(最高优)。
  • 用户可以接受请求发出到返回之间有等待时间,所以useEffect中触发的更新是默认优先级。

那么优先级怎么表示呢?用一个31位的二进制,被称为lane。

比如「同步优先级」和「默认优先级」定义如下:

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:

而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会根据电脑性能动态调整。

参考资料

[1]在线示例地址:

​https://codesandbox.io/s/immutable-glade-u0m6vv?file=/src/App.js。​

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2023-12-18 07:37:17

JavaScript防抖节流

2024-03-08 08:26:20

防抖节流delay​

2021-08-03 06:57:36

Js事件节流

2022-04-14 08:00:00

Cypress测试开发

2023-12-21 08:51:37

防抖节流Vue.js

2010-09-27 13:16:42

2021-12-09 10:57:19

防抖函数 Debounce

2023-08-29 08:28:43

React并发更新

2022-02-22 08:29:59

Vue前端防抖

2022-08-21 09:41:42

ReactVue3前端

2020-10-21 08:38:47

React源码

2011-03-07 10:26:04

FileZilla

2021-09-28 05:51:25

PostTaskReact浏览器

2016-11-23 16:48:20

react-nativandroidjavascript

2022-10-26 15:22:31

React组件User组件

2022-11-15 18:31:37

React

2023-01-01 23:42:22

React框架暗黑模式

2019-10-08 11:10:18

React自动保存前端

2018-06-13 16:38:33

React Nativ组件Android

2022-05-06 07:31:01

useEventReactHook
点赞
收藏

51CTO技术栈公众号