原文链接:https://github.com/taoliujun/blog/issues/19
useTransition
is a React Hook that lets you update the state without blocking the UI.
文档中简单一句话说明useTransition
的用途:不阻塞UI的情况下更新状态。
解决什么问题?
正常代码下,JavaScript是单线程的,所以执行一段耗时的代码,会阻塞UI的渲染,导致页面卡顿。React提供了时间切片的功能来尽量确保一帧中有充足的时间来渲染UI,而useTransition
就是在这个基础上,可以在不阻塞UI的情况下使用时间分片特性更新状态。
一个例子
先看下卡顿是如何形成的,一个简单的代码,每500ms,更新name
状态。另外点击按钮的时候,更新一系列状态并渲染到dom中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| const getDatas = () => { const datas = []; for (let i = 1; i <= 2000; i += 1) { const s = Math.random() * Math.random(); datas.push(s); } return datas; };
const Main: FC = () => { const [name, setName] = useState('world');
const [datas1, setDatas1] = useState<number[]>([]); const [datas2, setDatas2] = useState<number[]>([]); const [datas3, setDatas3] = useState<number[]>([]); const [datas4, setDatas4] = useState<number[]>([]); const [datas5, setDatas5] = useState<number[]>([]); const [datas6, setDatas6] = useState<number[]>([]); const [datas7, setDatas7] = useState<number[]>([]); const [datas8, setDatas8] = useState<number[]>([]);
const onClick1 = useCallback(() => { setDatas1(getDatas()); setDatas2(getDatas()); setDatas3(getDatas()); setDatas4(getDatas()); setDatas5(getDatas()); setDatas6(getDatas()); setDatas7(getDatas()); setDatas8(getDatas()); }, []);
useEffect(() => { window.setInterval(() => { setName(`world ${Math.random()}`); }, 500); }, []);
return ( <div> hello {name} <br /> <button onClick={onClick1}>click me</button> <br /> <div> {datas1.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas2.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas3.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas4.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas5.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas6.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas7.map((v) => { return <div key={v}>{v}</div>; })} </div> <div> {datas8.map((v) => { return <div key={v}>{v}</div>; })} </div> </div> ); };
|
首先不点击按钮,观察5秒,没有卡顿现象,性能表现如图:
可以看到有几个微微凸起的黄色点,对应着每次的hello
状态更新和渲染,它们的执行时间都在1ms,没有超过一帧的时间。
选取其中一个黄色的点,查看它的详情。React的调度器、协调器、渲染器创建了对应的任务,分步执行了任务,具体可阅读React架构的相关文章。
然后连续点几次按钮,hello
的渲染出现明显的卡顿,性能表现如图:
在性能图中截取的一段时间中,黄色是脚本执行时间,灰色是UI渲染时间,白色是空闲时间(我停止点击了一会儿),在每帧里,要跑完所有的状态变更和UI渲染,datas
系列的状态变更和渲染占据了大量的时间,基本是阻塞了hello
的状态变更和渲染。
只点击一次,性能表现如图:
可以看到几个Task,第一个Task就是在更新datas
系列状态和渲染,它占据了太多帧的时间,导致hello
的状态变更和渲染被推迟到后面的帧。
优化它
前面说到,React的架构中实现了时间切片,它允许开发者将不重要的变更推迟到后面的帧,这样就可以尽量保证优先执行默认任务。使用useTransition
改下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const [pending, startTransition] = useTransition();
const onClick1 = useCallback(() => { startTransition(() => { setDatas1(getDatas()); setDatas2(getDatas()); setDatas3(getDatas()); setDatas4(getDatas()); setDatas5(getDatas()); setDatas6(getDatas()); setDatas7(getDatas()); setDatas8(getDatas()); }); }, []);
|
再次连续点击按钮,卡顿现象明显减轻很多,性能表现如下:
一看起来,执行时间还是很长,那么为什么hello
渲染看起来不卡顿呢?
只点一次,看看性能表现:
查看几个Task的详情,发现datas
系列状态的更新,被分配在了多个Task中,中间还穿插了hello
的状态更新的任务。这也印证了useTransition
的实现背景:将不重要的任务通过时间切片架构,分配到多帧中,优先执行其他任务,从而实现不卡顿的目的。
注意事项
useTransition
是个hook,它的返回值还包括了一个pending状态,用来表示是否处于时间切片的过程中,可以用来优化UI,比如显示一个loading。
你也可以使用startTransition
这个util函数代替hook的使用。
时间切片架构是调度状态变化的,所以startTransition
的入参函数里,将状态更新标记为可切片,普通的代码段不会被标记。所以简单的说,你还是得将一个状态变更的执行时间控制在5ms内。