大家好,我是卡颂。
我女友是个铁憨憨,又菜又爱玩。
今天,她在刷完「时光代理人」后哭的稀里哗啦,准备给编剧寄刀片时被我拦了下来。
她一边抹着眼泪一边问我:“卡卡,你说时光真的可以重来?命运真是可以选择的么?”
我
:“可以的,React18
的新特性startTransition
就行。”
startTransition
的出现当然不是为了逆转命运,而是为了逆转React
的更新流程。
"在聊startTransition
的具体应用场景前,我先来聊聊React
是如何扬长避短的。"我一边摸着女票的小手一边说。
如果我们用「重编译时还是运行时」区分前端框架。那么Vue
和Svelte
就是「重编译时」的杰出代表。
在「编译时」,这两个框架可以分离模版语法中「变」与「不变」的部分,减少运行时的代码逻辑。
而React
由于使用JSX
(而非模版语法)描述视图,走的是「重运行时」的路线。
不是
React
不想在「编译时」做优化,奈何JSX
实在太灵活,做不到啊......
所以他的优化策略也都是偏「运行时」。
在「运行时」,最大的开销是:状态更新到视图变化中间的计算步骤。
这个步骤是通过「遍历Fiber
树」实现的。
常规的「运行时优化策略」,比如:
优化方向都是:减少遍历时需要遍历的Fiber
节点数量。
虽说性能优化的收益可以积少成多,但是React
团队早已不满足这种局部的小优化。
他们的思路是:
不同更新触发的视图变化显然是有轻重缓急的。
如果能区分更新的优先级,让高优更新对应的视图变化先渲染,那么就能在设备性能不变的情况下,让用户更快看到他们想看到的UI
。
比如:对于这样一个搜索下拉框:
用户期望:输入框输入的内容要实时反映在视图上(表现为输入内容不能卡顿)。
而结果下拉框的展示是可以有延迟的。
基于以上逻辑,React
希望提供一个API
,让用户告诉自己,哪些更新是「高优」的,哪些是「低优」的。
这样,React
就能知道优先渲染谁了。
这个API
,就是startTransition
。
接下来,我们用一个Demo[1]演示startTransition
的使用。
这个Demo
会渲染一棵「毕达哥拉斯树」。
拖动左边滑块会改变树渲染的节点数量。
拖动顶部滑块会改变树的倾斜角度。
最顶上有个帧雷达,可以实时显示更新过程中的掉帧情况。
当不点击Use startTransition
按钮,拖动顶上的滑块。
可以看到:拖动并不流畅,顶上的帧雷达显示掉帧(出现黄色、红色扇面)
当点击Use startTransition
按钮,拖动顶上的滑块。
可以明显看到:拖动变流畅,顶上的帧雷达显示掉帧的情况变少
让我们节选Demo
的代码看看,究竟发生了什么。
首先,控制滑块、树倾斜角度、要渲染的节点数量是分离在不同state
中的:
// 左侧滑块的state
const [treeSizeInput, setTreeSizeInput] = useState(8);
// 控制渲染节点数量的state
const [treeSize, setTreeSize] = useState(8);
// 顶部滑块的state
const [treeLeanInput, setTreeLeanInput] = useState(0);
// 控制树倾斜角度的state
const [treeLean, setTreeLean] = useState(0);
// startTransition的hook版本
const [isLeaning, startTransition] = useTransition();
当拖动顶上的滑块(改变树的倾斜角度)会调用changeTreeLean
方法:
function changeTreeLean(event) {
const value = Number(event.target.value);
setTreeLeanInput(value); // update input
// update visuals
if (enableStartTransition) {
startTransition(() => {
setTreeLean(value);
});
} else {
setTreeLean(value);
}
}
该方法会改变两个state
:
setTreeLeanInput
改变顶部滑块位置相关的state
—— treeLeanInput
setTreeLean
改变树的倾斜角度相关的state
—— treeLean
是否点击Use startTransition
按钮的区别,就在于setTreeLean
是否会被作为startTransition
的回调执行:
// 是否开启startTransition
if (enableStartTransition) {
startTransition(() => {
setTreeLean(value);
});
} else {
setTreeLean(value);
}
当作为startTransition
的回调执行时,setTreeLean
改变的状态(treeLean)对应的视图变化(即:改变树的倾斜角度)会被视为「低优先级的更新」。
即使其与改变滑块状态的方法(setTreeLeanInput)在同一上下文中执行,
由于其优先级较低,React
会优先处理「改变滑块状态」对应的视图变化。
表现为:滑块的滑动不卡顿。
铁憨憨
:“这么酷炫的功能实现起来一定很复杂吧?”
“恰恰相反,依赖于React
底层实现的优先级调度模型,startTransition
的实现其实很简单!”
以刚才的代码为例,如果加上console.log
打印:
console.log(1);
startTransition(() => {
console.log(2);
setTreeLean(value);
});
console.log(3);
那么会依次输出:123
startTransition
做的事情很简单,类似这样:
let isInTransition = false
function startTransition(fn) {
isInTransition = true
fn()
isInTransition = false
}
也就是说,当调用startTransition
,在其上下文中获取到的全局变量isInTransition
为true
。
如果startTransition
的回调函数fn
中包含更新状态的方法(比如上文Demo
中的setTreeLean
),
那么这次更新就会被标记为isTransition
,类似这样:
// 调用setTreeLean后会执行的方法(伪代码)
function setState(value) {
stateQueue.push({
nextState: value,
isTransition: isInTransition
})
}
代表这是一个低优先级的过渡更新。
接下来,就是React
内部的调度、批处理与更新流程了。
今天,我们讲了:
React
为了弥补自身弱编译时的缺点,在运行时作出的努力startTransition
本质是让开发者手动标记更新的优先级startTransition
的实现原理铁憨憨
:”原来React
为了性能优化做了这么多努力,好复杂啊,我还是用Vue
吧!“
我
:“可不是嘛,React
已经在朝着实现一个浏览器的方向发展了。”
此时,女朋友眼角的泪痕已干,讲React
知识真是哄女孩子不哭的好办法呢!
[1]
Demo: https://swizec.com/blog/a-better-react-18-starttransition-demo/