引言
React 系列继续,今天来聊一聊 transition
。话不多说,我们先用一个例子(React 18)来引入今天的主题:
import {useState, memo} from 'react'
const HeavyItem = memo(({query}) => {
for (let i = 0; i < 99999; i++) {}
return <div>{query}</div>
})
export default function App() {
const [inputValue, setInputValue] = useState('')
const handleChange = (e) => {
setInputValue(e.target.value)
}
return (
<div style={{paddingLeft: 100, paddingTop: 10}}>
<input value={inputValue} onChange={handleChange} />
<div>
{[...new Array(5000)].map((_, i) => (
<HeavyItem key={i} query={inputValue} />
))}
</div>
</div>
)
}
上面例子模拟了一个关键词搜索的应用,注意到其中的每一项搜索结果 HeavyItem
中,我们都空循环了 10 万次,用于模拟耗时的渲染过程。所以,我们在搜索的时候会感觉到有明显的卡顿现象:
根本原因在于搜索列表的渲染是一个非常耗时的操作,整个 React 应用的更新都被其所阻塞。但其实列表的更新可以稍后一些,而搜索关键字在 input
中的更新必须足够及时才能使得用户使用起来感觉比较流畅,也就是两个更新的优先级是有先后的。而 transition
的出现,就是为了解决这一类的问题。
useTransition
使用
我们通过 React 提供的 useTransiton
来优化上面的例子:
import {useState, useTransition, memo} from 'react'
const HeavyItem = memo(({query}) => {
for (let i = 0; i < 99999; i++) {}
return <div>{query}</div>
})
export default function App() {
const [inputValue, setInputValue] = useState('')
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const handleChange = (e) => {
setInputValue(e.target.value)
startTransition(() => {
setQuery(e.target.value)
})
}
return (
<div style={{paddingLeft: 100, paddingTop: 10}}>
<input value={inputValue} onChange={handleChange} />
<div>
{isPending
? 'Loading'
: [...new Array(5000)].map((_, i) => (
<HeavyItem key={i} query={query} />
))}
</div>
</div>
)
}
可以看到,现在搜索体验非常丝滑了:
实现原理
当我们在输入框中输入 a
时,会触发 handleChange
:
-
调用
setInputValue
产生一个更新任务(假设为inputUpdate1
)。 -
调用
startTransition
,首先会以当前优先级setPending(true)
(更新任务假设为pendingTrueUpdate1
),然后将优先级降低并setPending(false)
(更新任务假设为pendingFalseUpdate1
)以及调用回调函数执行setQuery
(更新任务假设为queryUpdate1
)。 -
React 会处理优先级较高的
inputUpdate1
和pendingTrueUpdate1
,此时页面 input 框中的内容得到更新,并显示 loading。
-
更新渲染完成后,会开始处理 pendingFalseUpdate1
和queryUpdate1
,由于此时需要渲染非常昂贵的列表,React 的 Render 过程可能会需要若干个时间切片才能处理完。
-
当用户继续输入 b
,由于步骤 4 中 React 是使用时间切片的方式来处理,所以当某个时间切片结束后,React 会把控制权交出,用户输入能够得到响应,此时又会触发如下更新任务:
// 高优先级
inputUpdate2
pendingTrueUpdate2
// 低优先级
pendingFalseUpdate2
queryUpdate2
React 发现有高优先级的更新插入,会取消掉步骤 4 中正在进行的更新任务,开始处理 inputUpdate2
和 pendingTrueUpdate2
:
6. 用户没有继续输入,则会将所有低优先级更新任务继续处理完。
performance
面板,我们可以更加直观地看到整个过程:
Why not debounce?
在 transition
出现之前,我们很容易会想到用 debounce
(防抖)来解决这样的问题:
import {useRef, useState, memo} from 'react'
function debounce(fn, wait = 300) {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
console.log('trigger')
fn.apply(this, args)
}, wait)
}
}
const HeavyItem = memo(({query}) => {
for (let i = 0; i < 99999; i++) {}
return <div>{query}</div>
})
export default function App() {
const [inputValue, setInputValue] = useState('')
const [query, setQuery] = useState('')
const debouncedSetQuery = useRef(debounce(setQuery))
const handleChange = (e) => {
setInputValue(e.target.value)
debouncedSetQuery.current(e.target.value)
}
return (
<div style={{paddingLeft: 100, paddingTop: 10}}>
<input value={inputValue} onChange={handleChange} />
<div>
{[...new Array(5000)].map((_, i) => (
<HeavyItem key={i} query={query} />
))}
</div>
</div>
)
}
但实际上 debounce
并不能解决这个问题。如下所示,我们先输入 a
,等到 trigger
打印后,继续输入 bcdef
,很明显后面输入的内容并没有立刻渲染出来:
原因在于 debounce
只是减少了 setQuery
的调用,但是治标不治本,一旦 setQuery
调用触发了更新,那 React 的渲染过程还是会阻塞用户交互。
从 performance
监控面板发现有两个耗时超过 1s 的任务(React 默认不会开启 Concurrent 模式,所以这里没有时间切片),分别对应着输入 a
和 bcdef
触发的更新。
俗话说得好,“解铃还须系铃人”。React 通过虚拟 DOM、协调算法等手段给广大前端程序员的开发带来巨大便利的同时也引入了一些成本,通过外部手段很难“根治”病因,还是得官方出马才能解决问题。
原文始发于微信公众号(前端游):体验一把 React transition
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/282262.html