React如何原生实现防抖?
The following article is from 魔术师卡颂 Author 长得帅掉头发卡颂
点击关注公众号,一周多次包邮送书
作为前端,想必你对防抖(debounce
)、节流(throttle
)这两个概念不陌生。
在React18
中,基于新的并发特性,React
原生实现了防抖的功能。
今天我们来聊聊这是如何实现的。
useTransition Demo
useTransition
是一个新增的原生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
推荐阅读
• 竟然有一半的人不知道 for 与 foreach 的区别???• 使用MQ的时候,怎么确保消息100%不丢失?• 协程到底有什么用?6种I/O模式告诉你!• Elasticsearch 写入优化,从 3000 到 8000/s,让你的 ES 飞起来!• 4种 Redis 集群方案介绍+优缺点对比• mysql插入数据会失败?为什么?
👇更多内容请点击👇