Skip to content

[译] 利用 React 18 中引入的 startTransition API,即使在大屏幕更新时也能保持 React 应用程序的响应速度

React 18 最重要的更新是并发渲染。并发是指通过确定任务的优先级来同时执行多个任务的能力。Dan Abramov 通过一个简单的电话类比很好地解释了这一概念。

React 18 公开了一些 API,允许用户对并发性进行一些控制。其中之一就是 startTransition API,它表示包在 startTransition 中的操作可能需要时间。

让我们详细了解一下 startTransition API。

有时,我们会遇到一些应用程序由于繁重或复杂的操作而变得反应迟钝。

例如,我们要从一个大列表中搜索一张照片。当我们在搜索输入框中键入照片名称时,我们希望键入的字符能毫无延迟地出现在屏幕上。当我们不能快速看到字符时,我们脑海中出现的第一个反应就是 "啊,这个应用程序太慢了!",这让我们感到沮丧。

为什么搜索会有点延迟?

让我们看一下代码。

jsx
import React, { useState } from 'react'
import PhotoCard from './PhotoCard'
import { Container, TextField } from '@material-ui/core'
import { makeStyles } from '@material-ui/styles'

const SearchPhotos = () => {
  const useStyles = makeStyles({
    container: {
      marginTop: '100px',
    },
  })
  const [title, setPhotoTitle] = useState('')
  const classes = useStyles()

  const onChange = (e) => {
    // Urgent update
    setPhotoTitle(e.target.value)
  }

  return (
    <Container className={classes.container}>
      <TextField
        id="standard-basic"
        label="Search by photo title"
        onChange={onChange}
        value={title}
      />
      <PhotoCard searchParam={title} />
    </Container>
  )
}

export default SearchPhotos
jsx
import React, { useEffect, useState } from 'react'
import photosJson from './photos.json'
import PhotoListCard from './PhotoListCard'

const PhotoCard = React.memo(({ searchParam }) => {
  const [photos, setPhotos] = useState()

  const fetchPhotos = (title) => {
    return new Promise((res) => {
      setTimeout(() => {
        if (!title) {
          return res(photosJson)
        }
        return res(photosJson.filter((photo) => photo.title.includes(title)))
      }, 500)
    })
  }

  useEffect(() => {
    fetchPhotos(searchParam).then((res) => {
      // Non-urgent update
      setPhotos(res)
    })
  }, [searchParam])

  const photoData = photos?.map((p) => ({
    key: p.id,
    name: p.title,
    thumbnailUrl: p.thumbnailUrl,
  }))

  return <PhotoListCard data={photoData} />
})

export default PhotoCard

每当用户在搜索中键入照片名称时,都需要进行两次不同的更新。

首先,我们将输入的值保存在状态中。

jsx
const onChange = (e) => {
  // Urgent update
  setPhotoTitle(e.target.value)
}

然后,我们利用存储的值来搜索照片。

jsx
useEffect(() => {
  fetchPhotos(searchParam).then((res) => {
    // Non-urgent update
    setPhotos(res)
  })
}, [searchParam])

第一次更新是紧急更新,目的是更改输入字段的值并显示输入的字符。第二个更新是显示结果。

React 18 之前,所有更新都被视为紧急更新。虽然用户认为显示搜索结果可能需要时间,但两个更新会同时呈现。这将阻止用户看到反馈,使其感觉有点反应迟钝。

使用优先更新 startTransition API 来改善用户交互体验。

新的 startTransition API 有助于将更新分为 紧急非紧急 两种。点击、选择等需要立即响应的事件应视为紧急事件。显示搜索结果、文本高亮等其他不需要立即响应的更新可标记为过渡或非紧急更新。这可以通过将过渡包入 startTransition 来实现。

jsx
const onChange = (e) => {
  const value = e.target.value
  setPhotoTitle(value)
  startTransition(() => {
    setSearchQuery(value)
  })
}

我们可以使用 setTimeout 来代替 startTransition API 吗?

我们会考虑使用 setTimeout 来延迟搜索结果,如下所示

jsx
const onChange = (e) => {
  const value = e.target.value
  setPhotoTitle(value)
  setTimeout(() => {
    setSearchQuery(value)
  }, 0)
}

Debouncing(防抖)和 Throttling(节流)当然也可以使用其他技术。

让我们看看使用 startTransition 比其他选项的优势。

setTimeout 不同的是,startTransition 不会在稍后进行调度。传递给 startTransition 的函数会同步运行,但其中的任何更新都会被标记为 过渡React 会根据这些信息决定如何呈现更新。

与使用 setTimeout 包装的情况相比,React 会更早开始呈现更新。在速度快的设备上,两次更新之间会有一点延迟。而在慢速设备上,延迟会更大,但用户界面仍会保持响应。

如何处理待定的过渡?

为了让用户了解正在进行的后台工作,React 提供了 isPending 标志,允许我们在用户等待时显示一个 Loading。

我们可以如下所示修改 SearchPhotos 组件,以显示 Loading

jsx
const SearchPhotos = () => {
  const useStyles = makeStyles({
    container: {
      marginTop: '100px',
    }
  });
  const [title, setPhotoTitle] = useState("");
  const [searchQuery, setSearchQuery] = useState("");
  const [isPending, startTransition] = useTransition();
  const classes = useStyles();

  const onChange = (e) => {
    const value = e.target.value;
    setPhotoTitle(value);
    startTransition(() => {
      setSearchQuery(value);
    });
  };

  return (
    <Container className={classes.container}>
      <TextField id="standard-basic" label="Search by photo title" onChange={onChange} value={title}/>
      {isPending && <LinearProgress/>}
      <PhotoCard searchParam={searchQuery} />
    </Container>
  );
};

我们发现了关于以下主题的一些有趣的讨论

要了解有关 startTransition API 的更多信息,请查看 React working groups 讨论 和有关 startTransition API 行为的快速概述