2019.10.24, существуетReact Conf 2019В первый же день React официально выпустилаConcurrent
Первое раннее сообщество ModeПредварительный просмотр документа, интересно официально познакомиться с массой разработчиков React.
Как и прошлогодние React Hooks, хотя Concurrent все ещеэкспериментальныйДа, я думаю, на этот раз это не займет много времени...
Этот большой переезд проводится уже более четырех лет
Если целью React Hooks является улучшение опыта разработки, то режим Concurrent фокусируется на улучшении взаимодействия с пользователем., на первый взгляд это может мало повлиять на нашу разработку, внутри React менялся несколько раз.
Эта серия статей в основном исходит от официальногоПредварительный просмотр документа, специально подготовленный для первых пользователей React.
В этом месяце был выпущен исходный код Vue 3.0, и статьи, связанные с Nuggets, произвели настоящий фурор.Нет никаких причин, по которым React является такой «большой новостью» (хотя эта новость была известна 3 года назад)... Позвольте мне взять ведущий.
🎉Далее:Первый предварительный просмотр React Concurrent Mode Part 2: Parallel World of useTransition
Структура содержания статьи
- Что такое параллельный режим?
- Включить параллельный режим
- Что такое СУСПЕНС?
- Как работает саспенс
- Состояние асинхронной операции Cache Suspense
- Одновременно инициировать запросы
- обрабатывать состояние гонки
- обработка ошибок
- Саспенс-оркестровка
- Суммировать
- использованная литература
Что такое параллельный режим?
Это набор функций, которые помогут вашему приложению React реагировать, изящно адаптируясь к возможностям устройства пользователя и условиям сети.. Этот набор функций, который содержитоба направленияОптимизация:
1️⃣ привязка к процессору
Интенсивность ЦП относится к оптимизации согласования (координация или Diff).В параллельном режиме согласование может быть прервано, уступая место высокоприоритетным задачам, сохраняя отзывчивость приложения.
На прошлой неделе я опубликовал статью в преддверии React Conf 2019.«🔥Это, наверное, самый популярный способ открыть React Fiber (разрезка по времени)»🤓, если вы хотите узнать о режиме Concurrent, настоятельно рекомендуем начать с этой статьи!
Оптимизация, интенсивно использующая ЦП, остается совместимой с существующим кодом, с небольшим воздействием на новые API, и основное влияние заключается в отказе от некоторых хорошо известных методов жизненного цикла.
2️⃣ Привязка к вводу-выводу (привязка к вводу-выводу)
В основном оптимизирована обработка React асинхронности. Основное оружие этоSuspense
а такжеuseTransition
:
-
Suspense
- Новый метод асинхронной обработки данных. -
useTransition
- Предоставляет механизм для предварительного рендеринга, React может быть в «другой ветке».предварительный рендеринг, дождитесь поступления данных, а затем отобразите их за один раз, уменьшив отображение состояния промежуточной загрузки и дрожания/мерцания страницы.
В этой статье я не буду подробно объяснять, что такое режим Concurrent.В этой статье мы познакомимся с Suspense, а в следующей статье мы познакомим вас с useTranstion., быть в курсе.
Включить параллельный режим
Параллельный режим в настоящее время является экспериментальным, вы можете установить экспериментальную версию с помощью следующей команды:
npm install react@experimental react-dom@experimental
# or
yarn add react@experimental react-dom@experimental
Как упоминалось выше, это для первых пользователей, хотя API не должен сильно меняться и не должен использоваться в производственной среде.
Запустить параллельный режим:
import ReactDOM from 'react-dom';
ReactDOM.createRoot(
document.getElementById('root')
).render(<App />);
Следует также отметить, что после включения режима Concurrent ранее устаревшие методы жизненного цикла полностью недоступны, убедитесь, что ваш код был перенесен.
Что такое саспенс?
Все должны быть знакомы с Suspense, он уже есть в версии 16.5.Suspense
Это API, но обычно используют его для сотрудничестваReact.lazy
Реализовать разделение кода:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
React.lazy Это небольшой эксперимент, Suspense по-прежнему отлично работает.
Если вы переведете Suspense на китайский язык, это等待
,悬垂
,悬停
значение.React дает более формальное определение:
Suspense — это не «библиотека сбора данных», а机制
, библиотека сбора данных использует этот механизм, чтобы сообщить React, что данные не готовы, а затем React будет ждать их завершения, прежде чем продолжить обновление пользовательского интерфейса.. Кратко подытоживая, Suspense — это механизм асинхронной обработки, предоставляемый React, а не конкретная библиотека запросов данных.Это примитив асинхронного вызова собственного компонента, предоставляемый React.. Это важный игрок в наборе функций параллельного режима.
Теперь мы можем использовать Suspense еще круче, и поверьте мне, он станет вашим мечом в кратчайшие сроки. С его помощью вы можете запросить удаленные данные следующим образом:
function Posts() {
const posts = useQuery(GET_MY_POSTS)
return (<div className="posts">
{posts.map(i => <Post key={i.id} value={i}/>)}
</div>)
}
function App() {
return (<div className="app">
<Suspense fallback={<Loader>Posts Loading...</Loader>}>
<Posts />
</Suspense>
</div>)
}
Загрузить зависимые скрипты:
function MyMap() {
useImportScripts('//api.map.baidu.com/api?v=2.0&ak=您的密钥')
return (<BDMap />)
}
function App() {
return (<div className="app">
<Suspense fallback={<Loader>地图加载中...</Loader>}>
<MyMap />
</Suspense>
</div>)
}
Если внимательно посмотреть на приведенный выше код, можно увидеть две характеристики:
- 1️⃣ Нам нужно
Suspense
чтобы обернуть эти компоненты, содержащие асинхронные операции, и предоставить им回退(fallback)
. Во время асинхронного запроса отображается этот запасной вариант. - 2️⃣ Приведенный выше код для получения асинхронных ресурсов похож на синхронный вызов. Правильно, с Suspense мы можем
async/await
илиGenerator
То же самое, используйте «синхронный» стиль кода для обработки асинхронных запросов.
Это удивительно, правда? Как React это делает?
Как работает саспенс
ранеекто-то проанализировалSuspense
реализация, который использует ReactErrorBoundary
Реализован аналогичный механизм, а мозговая дыра очень большая.
🤔 Хм... если использоватьErrorBoundary
Если это так, ErrorBoundary можно использовать для перехвата исключений подчиненных компонентов.Когда мы выполняем асинхронные операции, мы можем выдать исключение, чтобы прервать работу рендеринга.Когда мы завершим асинхронную операцию, мы сообщим React, что мы готовы, пожалуйста, продолжайте рендеринг...
🤭Это объясняет, почему вы можете использовать синхронный стиль для обработки асинхронных операций без использования async/await и Generator, throw может прервать выполнение кода...
🤔 Но это «исключение» следует отличать от обычных исключений, и оно должно сообщать ErrorBoundary, что асинхронная операция готова, и позволять продолжить рендеринг дочерних узлов...
Я думаю, что поток должен быть таким:
На самом деле, лучшим готовым «объектом-исключением», который подходит для этого сценария, является обещание. Затем засучите рукава и реализуйте одно из них:
export interface SuspenseProps {
fallback: React.ReactNode
}
interface SuspenseState {
pending: boolean
error?: any
}
export default class Suspense extends React.Component<SuspenseProps, SuspenseState> {
// ⚛️ 首先,记录是否处于挂载状态,因为我们不知道异步操作什么时候完成,可能在卸载之后
// 组件卸载后就不能调用 setState 了
private mounted = false
// 组件状态
public state: SuspenseState = {
// ⚛️ 表示现在正阻塞在异步操作上
pending: false,
// ⚛️ 表示异步操作出现了问题
error: undefined
}
public componentDidMount() {
this.mounted = true
}
public componentWillUnmount() {
this.mounted = false
}
// ⚛️ 使用 Error Boundary 机制捕获下级异常
public componentDidCatch(err: any) {
if (!this.mounted) {
return
}
// ⚛️ 判断是否是 Promise, 如果不是则向上抛
if (isPromise(err)) {
// 设置为 pending 状态
this.setState({ pending: true })
err.then(() => {
// ⚛️ 异步执行成功, 关闭pending 状态, 触发重新渲染
this.setState({ pending: false })
}).catch(err => {
// ⚛️ 异步执行失败, 我们需要妥善处理该异常,将它抛给 React
// 因为处于异步回调中,在这里抛出异常无法被 React 捕获,所以我们这里先记录下来
this.setState({ error: err || new Error('Suspense Error')})
})
} else {
throw err
}
}
// ⚛️ 在这里将 异常 抛给 React
public componentDidUpdate() {
if (this.state.pending && this.state.error) {
throw this.state.error
}
}
public render() {
// ⚛️ 在pending 状态时渲染 fallback
return this.state.pending ? this.props.fallback : this.props.children
}
}
⚠️ Внимание,Приведенный выше код есть только вv16.6(不包括)
был действителен до, После официального запуска Suspense в 16.6, Suspense изолирован от обычного ErrorBoundary, поэтому его нельзя использовать в .componentDidCatch
Обещание захвачено в .Когда в компоненте генерируется исключение Promise, React ищет ближайшую приостановку, чтобы обработать его, если не найдено, React выдает ошибку.
Приведенный выше код довольно прост для понимания, верно? Давайте проигнорируем фактическую реализацию React, его внутреннее устройство, очевидно, намного сложнее, и эти сложности — не все, о чем нужно заботиться разработчикам. С помощью приведенного выше простого кода мы, по крайней мере, знаем, как ведет себя Suspense. Давайте проверим это сейчас:
function ComponentThatThrowError() {
throw new Error('error from component')
return <div>throw error</div>
}
function App() {
return (
<div className="App">
<ErrorBoundary>
<Suspense fallback={null}> {/* Suspense 不会捕获除Promise之外的异常,所以这里会被ErrorBoundary捕获 */}
<ComponentThatThrowError />
</Suspense>
</ErrorBoundary>
<ErrorBoundary> {/* 如果异步操作失败,这个ErrorBoundary可以捕获异步操作的异常 */}
<Suspense fallback={<div>loading...</div>}> {/* 这里可以捕获ComponentThatThrowPromise 抛出的Promise,并显示loading... */}
<ComponentThatThrowPromise />
</Suspense>
</ErrorBoundary>
</div>
)
}
В приведенном выше коде показано основное использование приостановки и обработки исключений. ты можешь пройти этоCodeSandboxДавайте фактически запустим пример.
Теперь взглянитеComponentThatThrowResolvedPromise
:
let throwed = false
function ComponentThatThrowResolvedPromise() {
if (!throwed) {
throw new Promise((res, rej) => {
setTimeout(() => {
throwed = true
res()
}, 3000)
})
}
return <div>throw promise.</div>
}
Суть приведенного выше кодаthrowed
иthrow new Promise
. В этом компоненте мы передаемthrow new Promise
Чтобы прервать рендеринг компонента, Suspense будет ждать, пока промис будет готов, а затем повторно рендерит.
Чтобы избежать повторного рендеринга, обещание выдается снова, что приводит к «бесконечному циклу». Здесь вам нужно использовать «кеш», чтобы указать, что асинхронная операция готова, чтобы избежать повторного создания исключений..
В приведенном выше примере используется глобальная переменная throws для кэширования состояния асинхронной операции. Но для компонентов глобальным состоянием является Anti-Pattern, и побочные эффекты заставят компоненты не использоваться повторно. Кроме того, если кеш выходит из жизненного цикла компонента, его станет трудно контролировать, как мы можем судить, действителен ли кеш?Как контролируется жизненный цикл кеша?.
Конечно, вы можете использовать Redux или другие менеджеры состояний для обслуживания этих кешей, но иногда мы не хотим использовать менеджеры состояний.
Можно ли кэшировать это состояние внутри компонента? Ответ - нет, по крайней мере, не сейчас, что можно объяснить реализацией пользовательской приостановки выше:Когда приостановка переключается на ожидание, исходное дерево компонентов будет выгружено, а все состояния компонентов будут потеряны..
Как бы неприятно это ни звучало, кажется, что перенос асинхронных операций в Suspense потребует некоторой работы.
Состояние асинхронной операции Cache Suspense
Как было сказано выше, мы не можем кешировать состояние асинхронных операций внутри компонента, поэтому теперь его можно размещать только снаружи, и можно рассмотреть такие решения:
- Глобальный кеш. например, глобальные переменные, глобальные менеджеры состояний (например, Redux, Mobx)
- Использование контекстного API
- состояние кэшируется родительским компонентом
Последние два будут описаны ниже.
Использование контекстного API
Давайте используем Context API в качестве примера, чтобы кратко представить, как кэшироватьSuspense
Статус асинхронной операции.
Сначала определите статус асинхронной операции:
На самом деле это состояние Обещанияexport enum PromiseState {
Initial, // 初始化状态,即首次创建
Pending, // Promise 处于pending 状态
Resolved, // 正常结束
Rejected, // 异常
}
// 我们将保存在 Context 中的状态
export interface PromiseValue {
state: PromiseState
value: any
}
теперь создайтеReact.Context
Конкретно для кэширования асинхронного состояния, для краткости, наш Context очень прост, этоkey-value
место хранения:
interface ContextValues {
getResult(key: string): PromiseValue
resetResult(key: string): void
}
const Context = React.createContext<ContextValues>({} as any)
export const SimplePromiseCache: FC = props => {
const cache = useRef<Map<string, PromiseValue> | undefined>()
// 根据key获取缓存
const getResult = useCallback((key: string) => {
cache.current = cache.current || new Map()
if (cache.current.has(key)) {
return cache.current.get(key)!
}
const result = { state: PromiseState.Initial, value: undefined }
cache.current.set(key, result)
return result
}, [])
// 根据key c重置缓存
const resetResult = useCallback((key: string) => {
if (cache.current != null) cache.current.delete(key)
}, [])
const value = useMemo(() => ({ getResult, resetResult, }), [])
return <Context.Provider value={value}>{props.children}</Context.Provider>
}
После того, как это подсветка, мы создаемusePromise
Хуки для инкапсуляции асинхронных операций и упрощения утомительных шагов:
/**
* @params prom 接收一个Promise,进行异步操作
* @params key 缓存键
* @return 返回一个包含请求结果的对象,以及一个reset方法, 用于重置缓存,并重新请求
*/
export function usePromise<R>(prom: Promise<R>, key: string): { data: R; reset: () => void } {
// 用于强制重新渲染组件
const [, setCount] = useState(0)
// 获取context值
const cache = useContext(Context)
// ⚛️ 监听key变化,并重新发起请求
useEffect(
() => {
setCount(c => c + 1)
},
[key],
)
// ️⚛️ 异步处理
// 从 Context 中取出缓存
const result = cache.getResult(key)
switch (result.state) {
case PromiseState.Initial:
// ⚛️初始状态
result.state = PromiseState.Pending
result.value = prom
prom.then(
value => {
if (result.state === PromiseState.Pending) {
result.state = PromiseState.Resolved
result.value = value
}
},
err => {
if (result.state === PromiseState.Pending) {
result.state = PromiseState.Rejected
result.value = err
}
},
)
// 抛出promise,并中断渲染
throw prom
case PromiseState.Pending:
// ⚛️ 还处于请求状态,一个任务可能有多个组件触发,后面的渲染的组件可能会拿到Pending状态
throw result.value
case PromiseState.Resolved:
// ⚛️ 已正常结束
return {
data: result.value,
reset: () => {
cache.resetResult(key)
setCount(c => c + 1)
},
}
case PromiseState.Rejected:
// ⚛️ 异常结束,抛出错误
throw result.value
}
}
Приведенный выше код не представляет особой сложности, он заключается в том, чтобы решить, выбросить ли промис или вернуть результат асинхронного запроса в соответствии со статусом текущего аномального запроса.
Используйте его сейчас, используйте его первымSimplePromiseCache
пакетSuspense
Компонент верхнего уровня , чтобы компонент нижнего уровня мог получить кеш:
function App() {
return (<SimplePromiseCache>
<Suspense fallback="loading...">
<DelayShow timeout={3000}/>
</Suspense>
</SimplePromiseCache>)
}
Малый измельчитель:
function DelayShow({timeout}: {timeout: number}) {
const { data } = usePromise(
new Promise<number>(res => {
setTimeout(() => res(timeout), timeout)
}),
'delayShow', // 缓存键
)
return <div>DelayShow: {data}</div>
}
Эффект от запуска приведенного выше кода следующий:
В этом разделе показано, как кэшировать асинхронные операции через Context API, что может быть сложнее, чем вы думаете, и управление этими кэшами вручную — действительно непростая задача (Что использовать в качестве ключа кеша, как определить, что кеш действителен, и как переработать кеш). В том числе официальный представитель React не дал идеального ответа, эта яма все еще остается для изучения сообществом.
Если вы не являетесь автором библиотеки, рядовым React-разработчикам не обязательно преждевременно обращать внимание на эти детали.Я полагаю, что многие сторонние библиотеки, связанные с React-запросами данных, вскоре последуют за Suspense.
У React официально есть экспериментальная библиотека:react-cache, в настоящее время использующий глобальный кэш LRU
Получить состояние кеша родительскому
Поскольку в подкомпонентах Suspense нет возможности кэшировать асинхронное состояние, упомянем родительский компонент, который позволяет избежать глобального состояния, не нужно учитывать управление жизненным циклом кеша, мы можем более гибко управлять этими состояниями, а также можем упростить низкоуровневые компоненты логика. По сравнению с Context API я лично считаю, что это более общий подход.
Итак, как это сделать? мы основаны наusePromise
, создать еще одинcreateResource
функция, которая больше не является хуком, но создаетресурсный объект, сигнатура функции следующая:
function createResource<R>(prom: () => Promise<R>): Resource<R>
createResource
вернутьResource
Объект:
interface Resource<R> {
// 读取'资源', 在Suspense包裹的下级组件中调用, 和上文的usePromise一样的效果
read(): R
// ⚛️外加的好处,预加载
preload(): void
}
⚛️ Объект Resource создается в родительском компоненте, а затем передается в подчиненный компонент через Props, а подчиненный компонент вызываетread()
способ чтения данных.对于下级组件来说 Resource 和普通的对象没什么区别,它察觉不出来这是一个异步请求
. В этом прелесть этого саспенса!
Кроме того, поскольку объект Resource создается в родительском компоненте, это имеет дополнительное преимущество:Перед отображением подчиненных компонентов мы можем выполнитьpreload()
Предварительное выполнение асинхронных операций.
createResource
выполнить:
export default function createResource<R>(prom: () => Promise<R>): Resource<R> {
// 缓存
const result: PromiseValue = {
state: PromiseState.Initial,
value: prom,
}
function initial() {
if (result.state !== PromiseState.Initial) {
return
}
result.state = PromiseState.Pending
const p = (result.value = result.value())
p.then(
(value: any) => {
if (result.state === PromiseState.Pending) {
result.state = PromiseState.Resolved
result.value = value
}
},
(err: any) => {
if (result.state === PromiseState.Pending) {
result.state = PromiseState.Rejected
result.value = err
}
},
)
return p
}
return {
read() {
switch (result.state) {
case PromiseState.Initial:
// ⚛️初始状态
// 抛出promise,并中断渲染
throw initial()
case PromiseState.Pending:
// ⚛️ 还处于请求状态,一个任务可能有多个组件触发,后面的渲染的组件可能会拿到Pending状态
throw result.value
case PromiseState.Resolved:
// ⚛️ 已正常结束
return result.value
case PromiseState.Rejected:
// ⚛️ 异常结束,抛出错误
throw result.value
}
},
// 预加载
preload: initial,
}
}
createResource
Использование также очень простое, создайте ресурс в родительском компоненте, а затем передайте его дочернему компоненту через реквизиты. Ниже показан компонент «Вкладки», который отображает три вложенные вкладки, поскольку одновременно может отображаться только одна вкладка, мы можем выбрать предварительную загрузку тех вкладок, которые не отображаются, чтобы повысить скорость их открытия:
const App = () => {
const [active, setActive] = useState('tab1')
// 创建 Resource
const [resources] = useState(() => ({
tab1: createResource(() => fetchPosts()),
tab2: createResource(() => fetchOrders()),
tab3: createResource(() => fetchUsers()),
}))
useEffect(() => {
// 预加载未展示的Tab数据
Object.keys(resources).forEach(name => {
if (name !== active) {
resources[name].preload()
}
})
}, [])
return (<div className="app">
<Suspense fallback="loading...">
<Tabs active={active} onChange={setActive}>
<Tab key="tab1"><Posts resource={resources.tab1}></Posts></Tab>
<Tab key="tab2"><Orders resource={resources.tab2}></Orders></Tab>
<Tab key="tab3"><Users resource={resources.tab3}></Users></Tab>
</Tabs>
</Suspense>
</div>)
}
Давайте наугад выберем подкомпонент и посмотрим на его реализацию:
const Posts: FC<{resource: Resource<Post[]>}> = ({resource}) => {
const posts = resource.read()
return (<div className="posts">
{posts.map(i => <PostSummary key={i.id} value={i} />)}
</div>)
}
Хорошо, этот метод намного лучше, чем Context API, и я лично предпочитаю эту форму. В этом режиме, поскольку ресурс передается извне, поведение компонента является детерминированным, его легко тестировать и использовать повторно.
Однако есть два сценария применения:
- Режим Context API больше подходит для сторонних библиотек запросов данных, таких как Apollo и Relay. В этом режиме API будет более лаконичным и элегантным. Ссылаться наAPI реле
- Режим createResource больше подходит обычным разработчикам для инкапсуляции собственных асинхронных операций.
Одновременно инициировать запросы
Как показано на рисунке выше, такие сценарии часто встречаются в реальных проектах, данные сложного интерфейса могут поступать из нескольких интерфейсов, например:
/**
* 用户信息页面
*/
function ProfilePage() {
const [user, setUser] = useState(null);
// 先拿到用户信息
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}
/**
* 用户时间线
*/
function ProfileTimeline() {
const [posts, setPosts] = useState(null);
useEffect(() => {
fetchPosts().then(p => setPosts(p));
}, []);
if (posts === null) {
return <h2>Loading posts...</h2>;
}
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Приведенный выше пример кода взят из официальной документации. код вышеfetchUser
иfetchPosts
Загружается последовательно. Мы хотим, чтобы страница загружалась как можно быстрее. Есть два решения этой проблемы:
- 1️⃣ Передайте fetchPosts начальству, используйте
Promise.all
одновременная загрузка - 2️⃣ Извлеките два компонента в отдельные компоненты и превратите их в родственные отношения, а не в отношения родитель-потомок. Это можно отображать одновременно, чтобы запросы можно было инициировать одновременно.
Давайте сначала посмотрим на 1️⃣:
function fetchProfileData() {
// 使用 promise all 并发加载
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
const promise = fetchProfileData();
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
{/* ProfileTimeline 变成了纯组件,不包含业务请求 */}
<ProfileTimeline posts={posts} />
</>
);
}
Выглядит хорошо, но есть недостатки в этом подходе:
- ① Асинхронные запросы должны быть подняты, а затем использованы
Promise.all
Пакет, думаю очень хлопотный, что делать со сложными страницами? - ② Теперь время загрузки зависит от самой продолжительной операции в Promise.all, как насчет рендеринга как можно быстрее? Загрузка fetchPosts может занять много времени, в то время как fetchUser должен завершиться быстро, если fetchUser завершится первым, он должен, по крайней мере, позволить пользователю сначала увидеть информацию о пользователе.
План 1️⃣ не особенно хорош, давайте взглянем на план 2️⃣:
function ProfilePage() {
return (<div className="profile-page">
<ProfileDetails />
<ProfileTimeline />
</div>)
}
2️⃣ Решение лучше перед Suspense.ProfileDetails отвечает за загрузку пользовательской информации, а ProfileTimeline отвечает за загрузку таймлайна.Они выполняются одновременно, не мешая друг другу.
Но у него есть и недостатки: загрузка страницы будет иметь два加载指示符
, Можно ли объединить? Не исключено, что ProfileTimeline будет завершена первой, пока ProfileDetails еще в кружочках, страница будет очень странной...
Теперь есть план 3️⃣:Suspense
🎉
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
Когда React отображает ProfilePage, он возвращает ProfileDetails и ProfileTimeline.
Первый рендеринг ProfileDetails. В это время ресурс не загружается, и выдается исключение promise, прерывающее рендеринг ProfileDetails.
Затем React пытается отобразить ProfileTimeline, что также выдает исключение promise.
Наконец, React находит ближайший Suspense к ProfileDetails и отображает Loading Profile...
Как и решение 2️⃣, Suspense поддерживает параллельные запросы и устраняет некоторые недостатки решения 2️⃣: есть только один индикатор загрузки, и если ProfileTimeline завершится первым, он не будет отображаться.
Более того, ниже представлена более гибкая стратегия отображения статуса загрузки приостановки.
обрабатывать состояние гонки
Даже если Javascript является однопоточным, может потребоваться обработка условий гонки, главным образом потому, что время асинхронных операций не может быть гарантировано..
Не продавайте слишком много, расскажите пример. Существует такой компонент, который полагается на переданный извне идентификатор для асинхронного извлечения данных:
function UserInfo({id}: {id: string}) {
const [user, setUser] = useState<User|undefined>()
/**
* ⚛️ 监听id变化并发起请求
*/
useEffect(() => {
fetchUserInfo().then(user => setUser(user))
}, [id])
return user == null ? <Loading /> : renderUser(user)
}
Что не так с кодом выше? Предполагая, что идентификатор менялся много раз, здесь будет инициировано несколько запросов, но порядок выполнения этих запросов не может быть гарантирован, что приведет к условиям гонки.Первый запрос может быть выполнен последним, что приведет к тому, что страница представить неверные данные.
Как с этим бороться? Также лучше решить, используя что-то вродеоптимистическая блокировкаМеханизмы. Мы можем сохранить идентификатор этого запроса.Если идентификатор несовместим в конце запроса, это означает, что был инициирован новый запрос:
function UserInfo({id}: {id: string}) {
const [user, setUser] = setState<User|undefined>()
const currentId = useRef<string>()
/**
* ⚛️ 监听id变化并发起请求
*/
useEffect(() => {
currentId.current = id
fetchUserInfo().then(user => {
// id 不一致,说明已经有新的请求发起了, 放弃
if (id !== currentId.current) {
return
}
setUser(user)
})
}, [id])
return user == null ? <Loading /> : renderUser(user)
}
В Suspense нет состояния гонки.Вышеприведенный код реализован с Suspense следующим образом:
function UserInfo({resource}: {resource: Resource<User>}) {
const user = resource.read()
return renderUser(user)
}
Я полагаюсь на, так лаконично! В UserInfo передается простой объект, без условий гонки.
А как насчет его родительских компонентов?
function createUserResource(id: string) {
return {
info: createResource(() => fecthUserInfo(id)),
timeline: createResource(() => fecthTimeline(id)),
}
}
function UserPage({id}: {id: string}) {
const [resource, setResource] = useState(() => createUserResource(id))
// ⚛️ 将id的监听迁移到了这里
useEffect(() => {
// 重新设置resource
setResource(createUserResource(id))
}, [id])
return (<div className="user-page">
<Suspense loading="Loading User...">
<UserInfo resource={resource.info} />
<Timeline resource={resource.timeline} />
</Suspense>
</div>)
}
Асинхронный запрос преобразуется в «ресурсный объект», который здесь является обычным объектом, пропущенным через Props, что отлично решает проблему состояния гонки асинхронных запросов...
Кроме того, Suspense также решает проблему: После выполнения асинхронной операции наша страница могла переключиться, в это время устанавливаем состояние компонента через setState, и React выкинет исключение:
Can't perform a React state update on an unmounted component.
, теперь проблема естественно решена
обработка ошибок
Что делать, если асинхронный запрос ненормальный? Мы уже говорили в разделе о принципе реализации Suspense выше, если асинхронный запрос не удался, React выкинет исключение, которое мы можем отловить через механизм ErrorBoundary.
Мы пишем компонент более высокого порядка, чтобы упростить процесс приостановки и обработки исключений:
export default function sup<P>(
fallback: NonNullable<React.ReactNode>,
catcher: (err: any) => NonNullable<React.ReactNode>,
) {
return (Comp: React.ComponentType<P>) => {
interface State {
error?: any
}
class Sup extends React.Component<P, State> {
state: State = {}
// 捕获异常
static getDerivedStateFromError(error: any) {
return { error }
}
render() {
return (
<Suspense fallback={fallback}>
{this.state.error ? catcher(this.state.error) : <Comp {...this.props} />}
</Suspense>
)
}
}
return Sup
}
}
Используй это:
// UserInfo.js
const UserInfo: FC<UserInfoProps> = (props) => {/* ... */}
export default sup(
<Loading text="用户加载中..."/>,
(err) => <ErrorMessage error={err} />
)(UserInfo)
Сокращен некоторый шаблонный код, он относительно лаконичен, верно?
Саспенс-оркестровка
Если на странице много Suspense, то крутится много кругов, а пользовательский опыт нехороший.
Но нам нелегко объединить их напрямую, потому что у каждой части разный приоритет загрузки и жизненный цикл, и принудительно привязывать к приостановке нехорошо. Например:
function UserPage() {
return (<Suspense fallback="loading...">
<UserInfo resource={infoResource} />
<UserPost resource={postResource} />
</Suspense>)
}
Предполагая, что UserPost необходимо разбить на страницы, каждый щелчок на следующей странице вызовет весьUserPage
загрузка... Это определенно неприемлемо...
Таким образом, параллельный режим представляет новый APISuspenseList
, используемый для управления состоянием загрузки нескольких приостановок. Мы можем выбрать стратегию отображения состояния загрузки в соответствии с реальной сценой.. Например
function Page({ resource }) {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<h2>Loading Foo...</h2>}>
<Foo resource={resource} />
</Suspense>
<Suspense fallback={<h2>Loading Bar...</h2>}>
<Bar resource={resource} />
</Suspense>
</SuspenseList>
);
}
ПредположениеFoo
Время загрузки5s
,иBar
Время завершения загрузки2s
. Эффекты различных аранжировок и комбинаций SuspenseList следующие:
revealOrder
Указывает порядок отображения, в настоящее время имеет три необязательных значения:forwards
, backwards
, together
-
forwards
- Отображение спереди назад. То есть предыдущий не загружается, а последний не будет отображаться, даже если последний Suspense завершит асинхронную операцию раньше времени, ему нужно дождаться завершения предыдущего выполнения. -
backwards
- В отличие от форвардов, они отображаются в порядке от конца к началу. -
together
- Отображается вместе после загрузки всех приостановок
КромеSuspenseList
Есть еще одно свойствоtail
, используется для управления свертыванием этих приостановок, имеет три значения.默认值
, collapsed
, hidden
- По умолчанию - показать все
-
collapsed
- Свернуто, чтобы показать только первую загружаемую приостановку -
hidden
- не показывает статус загрузки
Кроме того, SuspenseList можно компоновать, подчиненные SuspenseList могут содержать другие SuspenseList.
Суммировать
Главный герой этой статьиSuspense
, если предположитьReact Hooks
— это логический примитив повторного использования, предоставляемый React, ErrorBoundary — это примитив захвата исключений, тогда Suspense будет примитивом асинхронной операции React. Suspense + ErrorBoundary упрощает ручную обработку статуса загрузки и статуса исключения.
Приостановка может пониматься как прерывание рендеринга или приостановка рендеринга. Мы кратко обсудили принцип реализации Suspense: он просто использует механизм генерации исключений ErrorBoundary для прерывания рендеринга и взаимодействует с компонентом Suspense для возобновления рендеринга компонента после завершения асинхронной операции.
Однако при повторном рендеринге компонента (повторном входе) все состояния теряются, и состояние асинхронной обработки не может быть сохранено локально в компоненте, поэтому необходимо извне запросить кэширование состояния асинхронной обработки в глобальном или компонент верхнего уровня.
Кто-то может возразить, что React недостаточно чистый и функциональный. Я не осмеливаюсь комментировать, я не ярый поклонник функционального программирования, я думаю, что пока это может решить проблему лучше, не имеет значения, какая парадигма программирования. С тех пор, как вышел React Hooks, нет так называемых чисто функциональных компонентов. Для Suspense шаблон createResource также делает поведение компонента предсказуемым и проверяемым. Что касается других болевых точек, требуется дальнейшая практика и проверка.
Suspense очень увлекательна, она не только решает некоторые проблемы асинхронной обработки в прошлом, но и привносит новые методы разработки. Нетерпеливые студенты могут попробовать это в своих собственных экспериментальных проектах.
использованная литература
- Suspense for Data Fetching (Experimental)
- Concurrent Rendering in React - Andrew Clark and Brian Vaughn - React Conf 2018
- React: реализация и обсуждение приостановки
- react-cache