react 并发模式
React JS开发团队在几个月前宣布了一些激动人心的更改-React将获得“并发模式”。 本质上,这将允许React同时执行多个UI渲染。 当然,JavaScript是单线程的,真正的并发只是一种错觉,但是新功能将使Web应用程序(以及一旦响应React Native的本机应用程序成为本机应用程序)比现在更加省力,更轻松,并且无需自定义代码从开发人员那里来实现这一目标。
现在,React的实验版本中可以使用并发模式,因此让我们深入研究一下如何使用闪亮的新API。
在这篇文章中,我将向您展示如何通过useTransition
挂钩使用Suspense
API。 还有另一个钩子useDeferredValue
,它具有稍微不同但同样重要的目的,我将在后续文章中介绍。
但是现有的Suspense功能如何?
您会注意到Suspense
API自v16.6起就已经存在。 实际上,这是在React实验版本中扩展为执行更多操作的同一API。 在React 16.6中,Suspense仅可用于一种用途:使用React.lazy()
代码拆分和延迟加载组件
新方法-获取时渲染
这已经通过演讲和博客进行了很多讨论,并且已经在官方文档中进行了讨论,因此我将在其简要说明中进行介绍-Concurrent React允许我们实现“获取时渲染”模式,该模式将组件渲染为填充它们所需的数据同时获取。 React会在没有可用数据的情况下尽可能多地渲染,并在数据可用时立即渲染需要获取的数据的组件。 在这段时间内,这些组件被称为“已暂停”。
常用的现有方法是“先获取然后渲染”(先获取后再渲染,然后再渲染组件),然后先“先渲染然后获取”(先渲染后渲染),再渲染组件,然后组件本身获取填充其子代所需的数据。 这两种方法都较慢,并且具有许多需要解决方法的缺点。
设置
为此,我使用了一个基本的React应用程序,该应用程序是我通过Webpack和Babel手动配置的( 单击此处可查看我编写的操作指南 ),唯一的区别是正在运行:
npm i react@experimental react-dom@experimental --save
要获得实验版本,而不是安装react
和react-dom
的发行版本。
这也应该与用create-react-app
React应用程序一起工作,方法是将react
和react-dom
替换为其实验版本。
选择并发模式
由于并发模式改变了React从根本上处理组件的方式,因此您需要将index.js
的ReactDOM.render()
行更改为:
ReactDOM.createRoot(document .getElementById( 'root' )).render( < App /> );
这将在您的应用程序中启用并发模式。
我还设置了App.js
来渲染一个名为Data
的组件,并在其中进行演示。
import React from 'react' ;
import Data from './Data' ;
const App = () => {
return (
< div >
<p>React Concurrent Mode testing</p>
<Data />
</ div >
);
}
export default App;
演示
现在我们创建Data.js
import React, { useState, useTransition, Suspense } from 'react' ;
import DataDisplay from './DataDisplay' ;
import { dataFetcher } from './api' ;
const initialData = { read : () => { return { foo : "initial" } } };
const Data = () => {
const [data, setData] = useState(initialData);
const [count, setCount] = useState( 0 );
const [startDataTransition, isDataPending] = useTransition({ timeoutMs : 2000 });
const fetchNewData = () => {
startDataTransition( () => {
setData(dataFetcher())
})
}
return (
< div >
<Suspense fallback={<p>Loading...</p>}>
<DataDisplay data={data} />
<button disabled={isDataPending} onClick={() => fetchNewData()}>Click me to begin data fetch</button>
</Suspense>
<p>Counter: {count}</p>
<button onClick={() => { setCount(count + 1); }}> Click me to check if the app is still responsive</button>
</div>
)
}
export default Data;
分解如下:
dataFetcher
是一个返回“特殊”对象的函数,该对象使React知道在呈现依赖于此状态的组件时可以获取设置为该对象的状态。 如果数据尚未完成提取,则将这些组件“挂起”。 我们将研究如何在最后创建“特殊对象”。
initialData
显示数据加载完成后dataFetcher
返回的对象的格式。 它具有read
功能,该功能可将所需的数据返回给对象。 理想情况下, initialData
应该为最后加载的数据实现某种缓存功能,但是在这里我们只使用{ foo : "initial" }
。
当状态被更新/获取导致组件挂起时,必须使用useTransition
挂钩进行更新。 该钩子返回一对值-一个函数,该函数采用在其中设置状态的回调函数,以及一个布尔值,使我们知道何时发生过渡。
传递给useTransition
的参数是一个对象,该对象告诉React暂停组件之前要等待多长时间。 要理解它,请这样考虑:在屏幕上有一些数据,我们正在获取一些新数据来替换它。 我们希望在获取新数据时显示一个微调框,但是用户可以在显示该微调框之前一秒钟甚至半秒钟看到旧数据。 在此对象中提到了此延迟。
这在需要显示陈旧数据直到需要加载新数据之前非常有用,并且还可以防止微调器在快速数据获取操作中出现几分之一秒(导致抖动)。
让我们仔细看看Suspense
本身:
<Suspense fallback={<p>Loading...</p>}>
<DataDisplay data={data} / >
< button disabled = {isDataPending} onClick = {() => fetchNewData()}>Click me to begin data fetch </ button >
< /Suspense>
任何应该挂起的组件都包装在Suspense
组件内。 在fallback
道具内部,我们传递了应该显示的组件,而其中的组件正在等待数据。 通常,它是某种微调器或加载指示器,可以直观地向用户指示发生了什么事情,因此,它看起来好像页面没有对单击做出响应。
在这里,我使用了isDataPending
布尔值来在获取数据时禁用按钮,从而防止用户多次按下按钮并发送多个请求-我们从模式中获得了isDataPending
。
谈论页面保持响应-它确实如此。 页面上的所有JavaScript都将继续运行,而组件将被挂起并正在获取数据。 计数器和增加它的按钮可以用来确认这一点。
DataDisplay
是一个简单的组件,它接收数据并调用其读取函数并显示结果。
import React, { memo } from 'react' ;
const DataDisplay = ( { data } ) => {
return (
< h3 > {data.read().foo} </ h3 >
)
}
export default memo(DataDisplay);
memo
在此处用于防止该组件在父级重新渲染时重新渲染,并且对于并发模式正常工作至关重要。
最后,我们来看一下dataFetcher
和api.js
的其他内容
export const dataFetcher = ( params ) => {
return wrapPromise(fetchData(params))
}
const wrapPromise = ( promise ) => {
let status = "pending" ;
let result;
let suspender = promise.then(
r => {
status = "success" ;
result = r;
},
e => {
status = "error" ;
result = e;
}
);
return {
read() {
if (status === "pending" ) {
throw suspender;
} else if (status === "error" ) {
throw result;
} else if (status === "success" ) {
return result;
}
}
};
}
const fetchData = ( params ) => {
// In a real situation, use params to fetch the data required.
return new Promise ( resolve => {
setTimeout( () => {
resolve({
foo : 'bar'
})
}, 3000 );
});
}
如您所见, dataFetcher
只是返回wrapPromise(fetchData())
,而fetchData()
是一个对数据进行实际请求的函数。 在实际情况下,您将在fetchData()
使用fetch()
并将params
传递给它,或者从其他位置加载数据。 在这里,我使用setTimout
返回一个Promise
对象,该对象在返回{ foo : 'bar' }
之前有意引入了3秒的延迟。
wrapPromise
负责使事物与React集成,如果您以前使用过Promises,则应该简单明了。 如果获取成功,则返回结果;如果未获取成功,则返回错误;如果操作尚未完成,则返回“挂起”状态的Promise
。
所有这些加在一起导致:
![](https://i-blog.csdnimg.cn/blog_migrate/76dac385d0dd8a77c1f6c964da48a0c6.png)
最初显示的数据为“初始”。 然后,我单击按钮开始获取数据。 根据我们的配置,该按钮立即被禁用,并且2秒钟内没有任何React。 然后该组件挂起,显示后备“ Loading ...”。 然后,最终数据获取完成,并且组件更新为显示“ bar”。 在整个过程中,剩余的应用程序(在此处由计数器显示)保持活动状态。
最后, Suspense
可以在没有useTransition
钩子的情况下工作,但useTransition
是所需数据不属于状态。
结论
在这里值得一api.js
是, api.js
奇怪的特定功能并不是太重要,因为预计将来许多流行的数据提取库都将支持它们。 React也建议不要在生产中使用并发模式,因为它仍然是“实验性的”。
但是,预计很快利用并发模式将成为绕过UI中冗长操作的实际方法,从而允许创建用户体验,这些体验在处理能力和网络连接速度差异很大的许多设备上保持一致。
- 您可以在此处找到演示中使用的所有代码。
- 您可以在Twitter和LinkedIn上找到我。
翻译自: https://hackernoon.com/concurrent-react-using-suspense-and-usetransition-to-build-better-ux-cman2cdd
react 并发模式