1. Concurrent React

其实 React 16 开始就有 Concurrent 的概念了,尤其是对于 Fiber 的更新,引入了优先级队列的概念。React 17 中的车道模型,Lane 等核心就是为了实现 Concurrent mode 下异步可中断的更新。这些都不算新的内容量。

React 的三种启动模式:legacy、blocking、concurrent

此前如果要开启 Concurrent 模式,就需要这样使用 React 入口函数:

1
ReactDOM.createRoot(rootNode).render(<App />)

等下,React 18 好像也是这么开启吧…

可以使用 <StrictMode> 来进行 concurrent 相关的 debug,如果安装了 React DevTools > 4.18.0,那么第二次渲染期间的日志现在将以柔和的颜色显示在控制台中。

2. Automatic Batching

其实 React 16 就有了 Batch 的概念。其目的就是 React 性能优化最重要的一步:减少 render 次数。

在 React 18 之前,React 中的 Batch 只能在组件的生命周期函数或者合成事件函数中进行批处理。默认情况下,Promise、setTimeout 以及原生事件中不会对其进行批处理。如果需要,可以使用 unstable_batchedUpdates 来实现。

而在 React 18 中,所有的更新都讲自动批处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Before: only React events were batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will render twice, once for each state update (no batching)
}, 1000);

// After: updates inside of timeouts, promises,
// native event handlers or any other event are batched.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once at the end (that's batching!)
}, 1000);

如果要退出自动批处理,可以用: flushSync

1
2
3
4
5
6
7
8
9
function handleClick() {
flushSync(() => {
setCount(3);
setFlag(true);
});
// setCount 和 setFlag 为批量更新,结束后
setLoading(false);
// 此方法会触发两次 render
}

3. Transitions

Transition 的官方定义:

A transition is a new concept in React to distinguish between urgent and non-urgent updates.

  • Urgent updates reflect direct interaction, like typing, clicking, pressing, and so on.
  • Transition updates transition the UI from one view to another.

听起来就是用于优化任务优先级的东西,把特定更新标记为 Transition 来改善用户体验。

Updates wrapped in startTransition are handled as non-urgent and will be interrupted if more urgent updates like clicks or key presses come in.

那就是说,可以把移到后台,不是那么紧急的更新用 useTransition 包装一下。

1
2
3
4
5
6
7
8
9
10
import {startTransition} from 'react';

// Urgent: Show what was typed 【表单操作:紧急】
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results 【计算/网络请求:非紧急】
setSearchQuery(input);
});

在函数组件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useTransition } from 'react';

function App() {
const [isPending, startTransition] = useTransition();

const handleInput = (e) => {
setInputValue(e.target.value) // 紧急

startTransition(() => {
searchForData() // 非紧急
})
}
}

设计一个带有 Transition 的 Button 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Button({ children, onClick }) {
const [startTransition, isPending] = useTransition();

function handleClick() {
startTransition(() => {
onClick();
});
}

return (
<button onClick={handleClick} disabled={isPending}>
{children} {isPending ? '加载中' : null}
</button>
);
}

useTransition 有个可选参数,可以设定超时时间 timeoutMs,但目前的 TS 类型没有开放。

4. useDeferredValue

useDeferredValue lets you defer re-rendering a non-urgent part of the tree. It is similar to debouncing, but has a few advantages compared to it. There is no fixed time delay, so React will attempt the deferred render right after the first render is reflected on the screen. The deferred render is interruptible and doesn’t block user input.

这个 API 和 useTransition 很相似,都是用来标识一些非紧急的内容,延迟更新任务。但 useDeferredValue 是产生一个新的值,这个值作为延时状态(延迟更新的state):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import "./styles.css";
import { useState, useDeferredValue } from "react";

export default function App() {
let [value, setValue] = useState(0);
const deferredValue = useDeferredValue(value, { timeoutMs: 2000 });

return (
<div className="App">
<div>{deferredValue}</div>
<button onClick={()=>{setValue(deferredValue+1)}}>click me</button>
</div>
);
}

5. Suspense

Suspense 之前只在 React.lazy 时用于一些懒加载的组件的 UI loading 状态显示,但在 React 18 中

In React 18, we’ve added support for Suspense on the server and expanded its capabilities using concurrent rendering features.

官方说拿它和 useTransition 一起食用更加,大概是用 Suspense 来悬挂延后更新的任务吧。

Suspense in React 18 works best when combined with the transition API. If you suspend during a transition, React will prevent already-visible content from being replaced by a fallback. Instead, React will delay the render until enough data has loaded to prevent a bad loading state.

在 React DOM Server 中,还支持 streaming Suspense on the server

  • renderToPipeableStream: for streaming in Node environments.
  • renderToReadableStream: for modern edge runtime environments, such as Deno and Cloudflare workers.

6. useId

用于在客户端和服务器上生成唯一 ID,同时避免水合不匹配。

7. useSyncExternalStore

解决外部 store 开发时的撕裂问题(tear)。

8. useInsertionEffect

类似于 useLayoutEFfect,此时无法访问 DOM 节点的引用。

9. 移除了:已卸载组件更新状态警告

warning:Can’t perform a React state update on an unmounted component. …

这个非常常见的警告被移除。这个告警出现的本意是警告一些组件卸载后却没有把 event listener 去掉的场景,但有时候其他的一些异步更新 state 也会报这个错误。所以很多时候会写一些 isMounted 的 flag 来判断组件是否卸载,但其实没有必要。

10. 允许组件返回 undefined

在 Suspense fallback 中,传入 undefined 会被忽视,更新后将表现为和 null 一样的行为。

一些未来可期待的:

  • 更强的 Suspense
  • Server Component

还有一点比较感兴趣的是 React blog 提到的 <Offscreen> 组件

Another example is reusable state. Concurrent React can remove sections of the UI from the screen, then add them back later while reusing the previous state. For example, when a user tabs away from a screen and back, React should be able to restore the previous screen in the same state it was in before. In an upcoming minor, we’re planning to add a new component called that implements this pattern. Similarly, you’ll be able to use Offscreen to prepare new UI in the background so that it’s ready before the user reveals it.