目前 Concurrent 尚处于实验阶段,大部分文档尚未被翻译。我基于目前的官方文档,对 Concurrent 做一些介绍。html
上一篇是关于React Suspense for Data的介绍。介绍了 Suspense for Data 模式和现有的数据请求方式的一些区别。你们阅读本篇文章前,能够先阅读下这篇文章。react
一般咱们在更新状态时,咱们但愿在屏幕当即看到状态的变化。可是在有些状况下,咱们会但愿屏幕上的状态更新延迟。好比,从一个页面切换到另外一个页面时,另外一个页面的代码或者数据尚未加载,会显示一个苍白的 loading 页,是使人沮丧的。在这种状况下,咱们更愿意在上一个页面停留更长的时间。在之前的 React 中,实现会很困难。可是 Concurrent UI 模式的出现带来了新的可能。git
为何说 Concurrent 模式。带来了更好的交互体验。咱们来举一个例子, 在github中点击进入下一层的文件夹,并不会出现loading页,而是将页面保持在前一个状态,当数据请求完成后,才会显示新的状态。其实过多的loading,可能会形成页面的闪烁,用户体验并非很好。github
在使用 Concurrent UI 模式出现前,在按下切换按钮后,当前页面的状态当即消失,并当即出现加载态。用户体验并非很好。若是在请求数据响应时间较短的状况下,能够跳过中间的加载状态,直接显示下一个状态的页面,就行了。后端
使用 Concurrent UI 模式后,在请求数据响应时间较短的状况下(小于timeoutMs
),咱们能够跳过中间加载态,直接显示新的状态页面。函数
React提供的新的内置Hook,useTransitions
,能够实现这种模式。useTransitions
会返回两个值,startTransition
以及isPending
post
startTransition
, 是一个函数。告诉React,React能够延迟某个状态的更新(延迟进入Suspense的挂起状态,保持目前的状态)。isPending
, 是一个布尔值,告诉咱们状态是否正在过渡。而useTransitions
的配置项。timeoutMs
,则告诉React,咱们愿意为过渡等待的最大时间。性能
若是超过最大时间,则进入挂起状态,显示Suspense
的fallback
。可是若是过渡完成在超时前,则显示前一个状态,直到新状态过渡完成。优化
// 使用useTransition的例子
function Page () {
const [startTransition, isPending] = useTransition({
timeoutMs: 2000
});
const [resource, setResource] = useState(
initResource
);
const handleClickNext = () => {
startTransition(() => {
const nextId = getNextId()
setResource(http(nextId))
})
}
return (
<React.Fragment>
<button
disabled={isPending}
onClick={handleClickNext}
>Next Id</button>
<p>{isPending ? " Loading..." : null}</p>
<React.Suspense fallback={<h1>Loading...</h1>}>
<Figure resource={resource}/>
</React.Suspense>
</React.Fragment>
)
}
function App() {
return (
<div className="App">
<Page/>
</div>
)
}
复制代码
因为旧的状态组件出如今屏幕,因此咱们知道它是存在的。对于新的状态组件,它也存在在某处。使用startTransition
对状态更新进行包装。状态更新将会发咱们看不到的地方(相似平行宇宙的地方)。当新的状态准备完成,新旧状态会发生合并,渲染出如今屏幕上。spa
虽然新旧状态的组件是同时存在的,可是这不意味着它们是同时渲染的。计算机的并行计算,实际上是在极短的时间内,切换到不一样的任务进行计算。
function UserList ({ resource }) {
const list = resource.read();
return (
<ul> { list && list.map(item => <li key={item.id}>{item.name}</li>) } </ul>
)
}
function App() {
const [resource, setResource] = useState(initResource);
const handleClickRefresh = () => {
setResource(http())
}
return (
<div className="App">
<button onClick={handleClickRefresh}>刷新</button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
</div>
)
}
复制代码
当咱们浏览一个页面,并与之交互时,若是出现了没必要要的loading,这种体验是不愉悦的。咱们可使用 useTransitions
将状态更新,包装在 startTransition
中。点击刷新,不会出现烦人的loading。
const initResource = http()
function List ({ resource }) {
const list = resource.read();
return (
<ul> { list && list.map(item => <li key={item.name}>{item.name}</li>) } </ul>
)
}
function App() {
const [resource, setResource] = useState(initResource);
const [startTransition, isPending] = useTransition({
timeoutMs: 2500
});
const handleClickRefresh = () => {
startTransition(() => {
setResource(http())
})
}
return (
<div className="App">
<button onClick={handleClickRefresh}>刷新 { isPending && '加载中……' }</button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource}/>
</React.Suspense>
</div>
)
}
复制代码
如今感受好多了,点击刷新,不会出现苍白的loading页,数据正在内部加载,当数据准备好,它就会显示出来。
Transitions 是很常见的,任何能够致使组件被挂起的组件都应该被 useTransition
包裹起来。下面是一个 Button
组件的例子。经过将 Transitions 融合到组件设计中,能够避免大量重复无用的代码。
// Button组件
function Button (props) {
const [startTransition, isPending] = useTransition({
timeoutMs: 3000
});
const { onClick, children } = props;
const handleClick = () => {
startTransition(() => {
onClick()
})
}
return (
<button onClick={handleClick}> { children } { isPending && '……s' } </button>
)
}
复制代码
// 在App文件中,不须要再次重复useTransition的逻辑
function App() {
const [resource1, setResource1] = useState(initResource);
const [resource2, setResource2] = useState(initResource);
const handleRefresh1 = () => {
setResource1(http())
}
const handleRefresh2 = () => {
setResource2(http())
}
return (
<div className="App">
<Button onClick={handleRefresh1}>刷新列表1</Button>
<br/>
<Button onClick={handleRefresh2}>刷新列表2</Button>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource1}/>
</React.Suspense>
<hr/>
<React.Suspense fallback={<h4>loading……</h4>}>
<List resource={resource2}/>
</React.Suspense>
</div>
)
}
复制代码
useTransitions
,会时页面保持在当前的状态,使页面依然是可交互的。当数据准备完毕,进入Skeleton态。若是数据超时,则回退到Receded态。默认状况下,咱们的页面状态变化是 Receded -> Skeleton -> Complete
。可是 Receded
状态给用户的体验并很差,在使用 useTransitions
后。咱们首选的页面状态变化是 Pending -> Skeleton -> Complete
。
考虑下面这种状况,假设咱们的 "用户列表" 的接口响应速度老是很慢(须要5s的时间才能返回),后端同窗短期也没法优化。它会拖慢咱们整个页面进入 Skeleton
态的时间。让页面长期处于 Pending
态,迟迟不能进入下一个状态。(以下图所示)
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* UserList列表加载过慢 */}
<UserList resource={resource}/>
<NewsList resource={resource}/>
</React.Fragment>
)
}
function LoginPage ({ onClick }) {
return (
<div>
<h1>首页</h1>
{/* Button组件使用了useTransitions进行封装 */}
<Button onClick={onClick}>下一页</Button>
</div>
)
}
function App() {
const [tab, setTab] = useState('login')
const [resource, setResource] = useState(null);
const handleClick = () => {
setResource(http())
setTab('home')
}
let page = null
if (tab === 'login') {
page = <LoginPage onClick={handleClick}/>
} else {
page = <HomePage resource={resource}/>
}
return (
<React.Suspense fallback={<h1>loading……</h1>}>
<React.Fragment>
{page}
</React.Fragment>
</React.Suspense>
)
}
复制代码
咱们可能会首先想到,修改 useTransitions
的 timeoutMs
, 但这样一样无济于事,由于回退到 Receded
一样是很差的体验。
有没有什么好的办法,能够优化呢?咱们能够将慢速的组件,包裹在Suspense中,让其延迟加载,这样看起来好多了,页面不会长期停留在Pending
态。
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* 使用Suspense对慢速组件进行包裹 */}
<React.Suspense fallback={<h4>用户列表加载中……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
<NewsList resource={resource}/>
</React.Fragment>
)
}
复制代码
试想一下。当咱们已经在 Skeleton
态时(这是合并渲染的前提)。此时有两个响应将在短期内依次返回,好比用户列表在 1200ms 后返回,新闻列表在 1300ms 后返回。两个Suspense
将会依次结束挂起的状态(两个Suspense
是嵌套的)。咱们已经等待了 1200ms, 也不在意多等待100ms,因此为了减小页面的重绘次数,提高性能。React会合并它们,一块儿渲染,而不是两个列表组件依次渲染。可是若是间隔大于100ms,仍是会依次渲染。
小于等于100ms
function HomePage ({ resource }) {
return (
<React.Fragment>
{/* Title resource 在 1000ms后响应,页面进入 Skeleton 态,这是合并渲染的前提 */}
<Title resource={resource}/>
{/* News resource 在 1200ms后响应 */}
{/* NewsList 将等待 UserList 响应后一块儿渲染 */}
<React.Suspense fallback={<h4>加载信息……</h4>}>
<NewsList resource={resource}/>
{/* News resource 在 1300ms后响应 */}
<React.Suspense fallback={<h4>加载用户列表……</h4>}>
<UserList resource={resource}/>
</React.Suspense>
</React.Suspense>
</React.Fragment>
)
}
复制代码
大于100ms
不是全部的状态更新,都适合放在 useTransition
中。咱们首先来看一下谷歌翻译的例子。
在谷歌翻译中。咱们在左侧每输入一点内容,右侧都会给出翻译的结果。当左侧结束输入的时候,右侧会给出完整的翻译结果。咱们来试着还原下这个效果。
在第一次输入的时候,Translation
组件被挂起,页面显示 Suspense
的 fallback
。这种效果是不理想的,咱们应该在输入完成前,看到以前翻译的内容。
而且控制台会打印中,以下的警告
警告咱们更新,更新应该分为多个部分。一部分更新须要及时的反馈到页面上,而另外一部分更新应该包含在 Transition
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const handleChange = (e) => {
const query = e.target.value
setQuery(query);
setResource(http(query))
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻译中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
复制代码
若是咱们把全部的状态更新,都包含在 Transition
中呢?问题更大了,页面的更新将会变得很是缓慢。input
中value的变化斗都很是的卡顿。
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const [startTransition, isPending] = useTransition({
timeoutMs: 5000
})
const handleChange = (e) => {
startTransition(() => {
const query = e.target.value
setQuery(query)
setResource(http(query))
})
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻译中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
复制代码
正确的作法是应该区分,高优先级状态的更新(setQuery
),以及低优先级的状态更新(setResource
)。setQuery
是当即发生的,setResource
则须要过渡。
function App () {
const [query, setQuery] = useState(initQuery)
const [resource, setResource] = useState(initResource)
const [startTransition, isPending] = useTransition({
timeoutMs: 5000
})
const handleChange = (e) => {
const query = e.target.value
setQuery(query)
startTransition(() => {
setResource(http(query))
})
}
return (
<div>
<input value={query} onChange={handleChange}/>
<React.Suspense fallback={<h1>翻译中…………</h1>}>
<Translation resource={resource} />
</React.Suspense>
</div>
)
}
复制代码
useTransition
必须配合 Suspense
一块儿使用。startTransition
中的操做能够触发Suspense
,才会让页面进入 Pending
态。startTransition
,所触发的Suspense
,必须在startTransition
触发前挂载完成。(Suspense
必须提早包裹在startTransition
操做的外面)useTransition
中的状态更新,不该该包含高优先级的(须要及时更新的内容。)思考一个例子,咱们在一个页面中会请求两个接口,一个文章的接口,一个文章的留言接口。两个接口响应时间是随机的。这就意味着,可能文章的留言接口已经返回了,可是文章的接口尚未返回。给用户的视觉体验并很差。
解决这种问题,有两种思路,第一种是将,文章的接口和文章的留言接口,都放在同一个 Suspense
中。可是若是文章的接口提早返回,咱们没有理由去等待留言的接口返回后,而后再渲染页面。
<React.Suspense fallback={<h1>加载中……</h1>}>
<Article resource={resource}/>
<ArticleComments resource={resource}/>
</React.Suspense>
复制代码
更好的办法是使用 SuspenseList
, SuspenseList
会控制 Suspense
子节点的显示顺序。
当SuspenseList的revealOrder属性设置为forwards时,内容将会按照它们在VDOM树中的顺序显示,从前向后渲染,即便它们的数据以不一样的顺序到达。(若是前面返回时间,大于后面的,它们会一块儿显示)
function App () {
return (
<React.Fragment>
<React.SuspenseList revealOrder="forwards">
<React.Suspense fallback={<h1>文章加载中……</h1>}>
<Article resource={resource}/>
</React.Suspense>
<React.Suspense fallback={<h1>留言加载中……</h1>}>
<ArticleComments resource={resource}/>
</React.Suspense>
</React.SuspenseList>
</React.Fragment>
)
}
复制代码
当设置为backwards时,内容将会按照它们在VDOM树中的顺序显示,从前向后渲染
当设置为together时,内容会一块儿渲染。
另一点是,SuspenseList是能够进行组合的。
上面仅是做者本身的理解,若有错误请及时指出。