React 18 新特性 startTransition

497 阅读7分钟

译文📖

前言

在React18,我们介绍一种新的API——startTransition,它可以帮助你的web应用迅速响应,即使在大型模块的更新场景,也能保持迅速响应。这个新API通过一种“transition”的方式更新,很大程度上提升用户交互体验。在state转换过程中,React将允许你提供一个视觉上的反馈,同时当transition发生时,保持浏览器高效响应

startTransition解决了什么问题?

有时候,用户一些小的行为,比如点击一个button或输入一个字符,能引起屏幕上很多的变化,从而导致页面暂时挂起或者无法响应。

举个例子🌰

用户在输入框输入一些值的时候,会过滤返回一些列表数据。你可能需要存储用户输入的值,并将输入关联的返回过滤展示给用户。你的代码可能如下:

// 更新用户输入并且搜索结果
setSearchQuery(input)

在这个场景下,用户输入了一个字符,我们就更新input并且用input值去搜索结果返回给用户。在大型页面模块的更新中,会导致页面卡顿,用户输入下一个字符的时候会感觉响应迟钝。即使列表不是太长,列表项本身也可能很复杂并且每次用户击键时都不同,可能没有明确的方法来优化它们的呈现。

从概念上来说,这个问题其实是两个不同的更新。第一个更新是非常紧急的更新(urgent update)用于更改输入字段的值。第二个更新是不太紧急的更新(less urgent)去显示搜索结果。

// Urgent: 显示输入字段
setInputValue(input);

// Not Urgent: 显示结果
setSearchQuery(input);

我们希望第一个更新能立马响应,因为浏览器处理这些交互非常迅速。但是第二个更新可能有一点延迟,因为这里有很多事情需要去做。

在React 18之前,所有更新都会立即渲染,因为它们归类为紧急更新。这意味着上面两个状态会在同一时间渲染从而阻塞用户看到它们交互的反馈,直到所有都渲染完毕。我们缺少一种告诉React哪些更新是紧急的,哪些更新不是紧急的方法。

startTransition提供了什么帮助?

startTransition通过提供标记更新为“transitions”的能力,解决了这类问题。

import { startTransition } from 'react';
// Urgent: 显示输入字段
setInputValue(input);

// 标记任何在函数内的state更新为transitions
startTransition(()=>{
    // Transition: 显示搜索结果
    setSearchQuery(input);
});

被startTransition包裹的更新会被处理成不紧急的更新,并且可以被紧急更新打断,比如:点击事件、按键被触发等。如果一个transition被用户打断了,比如连续输入多个字符,react会丢弃陈旧的未完成的渲染,并且渲染最新的更新。

transitions不仅会保持大多数的交互敏捷,即使它们导致显著的UI更改,还能避免浪费时间在渲染毫不相关的内容上。

什么是transition?

我们将更新分为两类:

  • Urgent Updates 表示直接的交互,比如打字、点击按钮、按下按键等
  • Transition Updates 表示从一个UI过渡到另一个UI

像打字、点击按钮、按下按键等等这样的更新需要立即响应,以符合我们关于物理对象行为的直觉。否则,用户会觉得“出错了”。但是,transitions与这些更新不同,因为用户不希望在屏幕上看到每一个中间值。

举个例子🌰,当你在下拉框选中了一个过滤值,你期待这个过滤按钮立马对你的点击作出反应。但实际结果会单独转换。一个小小的延迟可能会难以察觉且通常是可以预料到的。如果在结果还没完全渲染完成时,此时再更改了过滤值,你只会关注最新的结果。

在一个典型的React应用里,大多数的更新理论上都是transition 更新。但为了向后兼容,transitions是可选的。默认情况下,React 18仍将更新处理为紧急更新,你可以通过startTransition标记一个更新为transition。

startTransition和setTimeout有什么不同?

通常一个共同的解决方法是通过setTimeout去处理以上的问题。

// 显示输入字段
setInputValue(input);

// 显示结果
setTimeout(()=>{
    setSearchQuery(input);
},0)

setTimeout会延迟第二个更新直到第一个更新渲染完成。节流和防抖是这种技术的常见变体。

一个很重要的不同是startTransition不像setTimeout那样延迟执行,它立即执行了。传给startTransition的函数同步执行,且其中任何的更新都会被标记为“transitions”。当React处理这些更新的时候,会通过标记信息去决定怎么渲染它们。这就意味着,我们比setTimeout更早地渲染更新。在一个网速不错的设备,在两个更新之间只会有很小的延迟。在一个网速很慢的设备,延迟会变大,但UI交互会保持敏捷。

另一个很重要的不同是一个大型模块更新,setTimeout会锁住页面直到timeout结束。如果当setTimeout触发时,用户仍在输入或者与页面交互,用户会被阻止与页面进行交互。但被startTransition标记的更新可以被打断,因此它不会锁住页面。startTransition让浏览器在渲染不同组件的时间缝隙中处理事情。如果用户输入有变化,React不会渲染用户不再感兴趣的内容。

最后,因为setTimeout延迟更新,需要展示一个loading,并且编写异步代码,这通常很脆弱。通过transition,React为你可以追踪等待中的state,根据转换的当前状态更新它,并且提供展示给用户loading的能力。

当transition正在等待,我应该做什么?

作为最佳实践,你会想告知用户有一些事情在后台处理。为此,我们提供了一个带着isPending标识的Hook。

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

当transition在等待中时,isPending值为true。用户在等待时,你可以展示一个spinner。

isPending && <Spinner />

包裹在transition里的state不需要来自同一个组件。比如,在搜索框里的spinner可以反映重新渲染搜索结果的进度。

为什么不写更快的代码呢?

写更快的代码和避免不必要的重新渲染仍然是提高性能的好方法。transitions 与之相辅相成。在重要的视觉变化上,它们能让UI迅速响应,比如,当显示一个新的模块。这很难用现有的策略去优化它。即使没有不必要的重新渲染,与把每个更新都视为紧急更新相比,transition仍然提供了一个更好的用户体验。

哪里可以使用statTransition?

你可以使用startTransition去包裹任何你想把它放到后台的更新。通常,这些更新分为两类:

  • Slow Rendering:React需要执行很多步骤去转换UI展示结果,这些更新消耗大量时间。
  • Slow Network:React需要等待从网络上回传的数据,这些更新在很慢的网络上消耗大量时间。此用例与Suspense紧密结合。

实操🔍

由于掘金无法上传视频,实操可以看下官方例子github.com/reactwg/rea…

参考链接

译文的原文:github.com/reactwg/rea…
官方实操:github.com/reactwg/rea…

一些思考🤔

batchUpdate是否与startTransition思想冲突了呢?

不冲突。startTransition是将batchUpdate中需要延迟互动的渲染抽出来优化,两者没有冲突一说。需要走批处理更新的继续走batchUpdate,延迟互动渲染的走startTransition。