react18 更新特性
# 01. Automatic Batching(自动更新)
- 自动批量更新state,减少渲染次数。react的batching 是将多个状态合并成一次重新渲染,如下,组件只会在handleClick被调用后,执行一次渲染。
生效范围:
React18之前 | React18以后 |
---|---|
react event handler | react event handler |
Promise | |
setTimeout | |
native event handler | |
... |
import React, { useState } from 'react'
export default function Index() {
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(0);
const handleClick = () =>{
setClicked(clicked+1)
setCount(count+1)
}
// 执行两次操作
const handleClick = () =>{
setTimeout(() => {
setClicked(clicked+1)
setCount(count+1)
},0)
}
return (
<div>
<button onClick={handleClick}>点击</button>
<div>
count: {count}
clicked: {clicked}
</div>
</div>
)
}
- 不同上下文中,调用多个状态更新,例如在promise,回调或者定时器中,react就不会进行batching操作,不会将两次更新合二为一,会执行两次更新,但实质上 我们可能只需要一次。在react 18中,所有这些用例都会被覆盖,同时会批量更新状态,无论上下文如何,都会自动更新。 这个与unstable_batchedUpdates行为一致 ,react会默认帮我们完成这个操作。
- 如果不希望进行batch更新操作,可以使用flushSync,它将在每次运行完我们传递给它的函数后,重新渲染组件,当我们按照下面实例方式编写,组件会被渲染两次。
import { flushSync } from 'react-dom'
const handleClick = () =>{
flushSync(() => {
setClicked(clicked+1)
},0)
flushSync(() => {
setCount(count+1)
},0)
}
# 02. Concurrent Mode(并发渲染)
- 渲染模型的变化
react18之前版本的渲染模型是线性的, 渲染都是一个接着一个被触发,并且还要被触发了就无法终止 。
react18以后,渲染被分了优先级,开发者决定哪部分渲染是低优先级的,可以被暂定,终中断的,哪部分的渲染是高优先级的,需要在主线程,也就是不可被中断的 react会在内部调度,来保证并发模式下UI的一致性,这样的话,即使有大型渲染任务也不会被卡住UI,可以有更高的用户体验
- 渲染可以被中断,继续,终止的
- 渲染可以在后台进行
- 渲染可以有优先级
- 不是新功能,而是一种新的底层机制
# 03. Transitions(过渡)
- 指定渲染优先级
Transitions(过渡)告诉react更新的优先级,在Concurrent模式上建立的
# TransitionsAPI
startTransition 或者hook 中useTransition
import { startTransition, useTransition } from 'react';
// 紧急状态
setInputValue(value)
startTransition(() => {
setSearchQuery(value)
})
// hook
const [isPending, startTransition] = useTransition();
// isPending 告知我们这个次优先级的渲染是否在等待中,如果需要标记某些渲染是低优先级的,可以用上述的两个API把state更新操作包裹起来,这样使react内部明确渲染的调度逻辑
场景:优先响应input的渲染,其次响应列表渲染 我们在搜索输入时更新,我们会期望他的值随着我们的输入变化而变化,而当我们输入时,搜索结果可能在1s内出现,这时我们将输入搜索的内容部分标记为紧急更新,而元素过滤的部分被标记为次要,也就是我们理解的【过渡】。过渡本身可能会被紧急更新中断,而之前不相关的过渡,将被忽略。这将告诉ui仅显示其最新状态,并跳过次要状态的更新,过渡,这个过程中计算可能慢,有时会返回中间值 或不相关的状态。我们下面代码所示,我们将input 框中的值的改变标记为紧急状态. 在过渡期会进行二次更新,因为它可能会触发缓慢更新,同时我们在输入时,会导致ui卡顿或者延迟,影响用户体验 。
startTransition 非常适应用于任何需要后台运行的更新,例如当遇到缓慢且复杂的渲染任务,或者当更新依赖于数据获取时,网速很慢的情况。
# 04. Suspense and SSR
- 更方便的组织并行请求和 loading状态的代码
# SSR
不使用SSR渲染,网站在第一次运行会白屏,这是因为浏览器需求请求并读取我们的javascrupt,这需要一些时间,接着才会加载组件,页面才能进行交互,使用SSR时,用户可以直观的看到应用程序外观,但在加载javascript时,依旧无法交互,其工作原理是先在服务器上的渲染所有组件,然后将结果作为HTML发送给浏览器 ,接着像往常一样加载javascript,而html通过激活(Hydration)作用后,恢复可交互的状态,这会将静态html元素转化为动态react组件。这种方式存在的主要问题是,只要javascript还未被获取加载,html就还未激活,页面无法进行交互(interactive)。
为了解决这个瀑布问题,react18 SSR提供了两个新功能:
- Streamimg HTML (流式html):简而言之,流式html意味着服务器可以在你的组件渲染时,发送组件的片段,这方式可以通过Suspense实现,我们可以在应用程序中声明哪些部分需要加载更长的时间,哪些部分应该直接渲染。如果我们渲染一篇带有评论的文章,其中文章是页面的关键部分,我们可以直接发送要加载文章内容的html到浏览器,而无需等待评论。我们可以使用Suspense展示一个加载器,一旦评论准备好,react将发送新的HTML替代加载指示器。
<NavBar />
<SideBar />
<Article />
<Suspense callback={<Loader/>}>
<Components />
</Suspense>
- Selective Hydration(选择性激活):完全改变了规则 ,before:必须等待每个组件被渲染后,才能开始激活,从而导致代码分割问题,而现在组件使用Suspense进行包裹,不会再阻碍激活。
比如上述文章 评论组件使用Suspense包裹,就不会阻碍文章和其他组件被激活。每个准备好的组件都会开始被激活,一旦浏览器获取到需求的内容和javascript代码 ,评论也会被激活。另外特殊一点:组件被完全被激活之前与之交互,比如说单击了某处,react会记录下这次点击事件,并且把点击组件的Hydration优先级提高,因为它更紧迫。当组件hydration 完成后,react会重新触发之前记录的点击事件,让组件响应这个交互(确保了交互在我们激活完成后,再次被执行 保证它优于其他组件执行激活操作 )。如果没有其他高优先级的工作了,react会进行其他组件的Hrdartion部分(优先从组件树中靠前的位置)。
# Suspense
过去我们是如何获取数据并渲染的?
- fetch on render
- fetch then render
- fetch whiler render(Suspense)
# 基本原理
在suspense包裹的内容,如果react看到一个被throw出来的promise ,那么react会catch住它,并找到离他最近的Suspense组件 ,这样suspense就知道 它需要等这个promise完成 ,接着他就可以直接渲染fallbak的loading部分。在promise.resolve后 ,suspense就会渲染内部需要使用的组件,所以为了使用suspense ,数据请求的方法 是需要符合某种约定的,也就意味着我们的网络请求层 就需要适配React Suspense的这种约定,react希望更多的网络请求库能够适配 suspense这种机制 ,这样开发者就能更方便的使用这种功能,例如Relay, Next.js, Hydrogen, or Remix
function Component() {
if (data) {
return <div>{data.message}</div>
}
throw promise
// react will catch this, find the closest "Suspense" Component
}
React.createRoot(rootEL).render(
<React.Suspense callback={<div>Loading...</div>}>
<Component/>
</React.Suspense>
)
# 05. New Client and Server Rendering APIs
# React DOM 客户端 (opens new window)
- 新API 导出位置:react-dom/client
createRoot:为root创建以渲染或卸载的新方法,使用它代替ReactDOM.render. 没有它,React 18 中的新功能就无法工作
hydrateRoot: 为hydrate一个服务渲染应用程序的新方法。使用它而不是 ReactDOM.hydrate与新的 React DOM 服务器 API 结合使用。没有它,React 18 中的新功能就无法工作
createRoot 和 hydraRoot 都接受一个名为 onRecoverableError 的新选项,以便我们想在react从渲染 或者hydration期间的错误中恢复或日志记录时收到通知,默认情况下,在较旧的浏览器中,React使用reportError, 或console.error.
# React DOM Server (opens new window)
- 新API导出位置: react-dom/server,并且且完全支持在服务器上流式传输 Suspense
renderToPipeableStream:用于node环境的流式传输
renderToReadableStream:适用于现代边缘运行时环境,例如 Deno 和 Cloudflare worker
# 06. 新的严格模式行为
- 确保可重用状态:允许 React 在保留状态的同时添加和删除 UI 部分
例如,当用户从一个屏幕上移开并返回时,React 应该能够立即显示上一个屏幕。为此,React 将使用与以前相同的组件状态卸载和重新安装树。 此功能将为 React 应用程序提供更好的开箱即用性能,但要求组件能够对多次安装和销毁的效果具有弹性。大多数效果无需任何更改即可工作,但有些效果假定它们只安装或销毁一次。
为了帮助解决这些问题,React 18 为严格模式引入了一个新的仅限开发的检查。每当第一次安装组件时,此新检查将自动卸载并重新安装每个组件,并在第二次安装时恢复先前的状态。
如果没有这个改变,当一个组件挂载时,React 会创建效果
* React mounts the component.
* Layout effects are created.
* Effects are created.
从 React 18 开始使用严格模式,每当组件在开发中安装时,React 将立即模拟卸载和重新安装
* React mounts the component.
* Layout effects are created.
* Effects are created.
* React simulates unmounting the component.
* Layout effects are destroyed.
* Effects are destroyed.
* React simulates mounting the component with the previous state.
* Layout effects are created.
* Effects are created.
在第二次挂载时,React 将从第一次挂载恢复状态。此功能模拟用户行为,例如用户从屏幕上移开并返回,确保代码能够正确处理状态恢复
当组件卸载时,效果会正常销毁:
* React unmounts the component.
* Layout effects are destroyed.
* Effect effects are destroyed.
如何支持可重用性?
举个例子:
function ExampleComponent(props) {
useEffect(() => {
// Effect setup code...
return () => {
// Effect cleanup code...
};
}, []);
useLayoutEffect(() => {
// Layout effect setup code...
return () => {
// Layout effect cleanup code...
};
}, []);
// Render stuff...
}
这个组件声明了一些在挂载和卸载运行的Effect。通常,这些Effect只会运行一次(在最初安装组件之后),而清理功能会运行一次(在卸载组件之后)。在React 18 Strict模式下,将发生以下情况:
- React渲染组件
- React挂载组件
- Layout effect代码运行
- Effect代码运行
- React模拟隐藏或卸载的组件
- Layout effect清理代码运行
- Effect清理代码运行
只要Effect在其自身之后进行清理(必要时返回清理方法),这通常不会导致问题。大多数Effect至少有一个依赖项。因此,它们可能已经能够适应多次运行,并且可能不需要任何更改。
不过,仅在挂载上运行的Effect可能需要更改。从高层次来看,最有可能需要修改的影响类型分为两类:
- 卸载时需要清理的Effect。
- 只应运行一次的Effect(挂载时或依赖项更改时)。
需要清理的效果应该具有对称性。
无论是添加事件侦听器还是与某些命令式API交互,一般来说,如果Effect返回清理函数,那么它应该镜像设置函数。如今,许多组件使用下面所示模式的变体(variation)。
}
// A Ref (or Memo) is used to init and cache some imperative API.
const ref = useRef(null);
if (ref.current === null) {
ref.current = new SomeImperativeThing();
}
// Note this could be useLayoutEffect too; same pattern.
useEffect(() => {
const someImperativeThing = ref.current;
return () => {
// And an unmount effect (or layout effect) is used to destroy it.
someImperativeThing.destroy();
};
}, []);
}
如果上面的代码被卸载和重新装载,那么命令式的东西很可能会被破坏。(毕竟,它在第一次卸载后就被摧毁了。)为了解决这个问题,我们需要(重新)初始化mount上的命令。
// 不使用Ref来初始化一些必需的东西!
useEffect(() => {
// 在同一个effect里面声明SomeImperativeThing,销毁它
// 这样,如果组件被重新装载,它将被重新创建
const someImperativeThing = new SomeImperativeThing();
return () => {
someImperativeThing.destroy();
};
}, []);
有时,其他函数(如事件处理程序)也需要与命令进行交互。在这种情况下,可以使用ref来共享该值。
// 使用Ref保存该值,但在Effect中对其进行初始化
const ref = useRef(null);
useEffect(() => {
// 在同一个effect里面声明SomeImperativeThing,销毁它
// 这样,如果组件被重新装载,它将被重新创建。
const someImperativeThing = ref.current = new SomeImperativeThing();
return () => {
someImperativeThing.destroy();
};
}, []);
const handeThing = (event) => {
const someImperativeThing = ref.current;
// 现在我们可以使用了
};
尽管不常见,但命令式API可能还需要与其他组件共享。在这种情况下,可以使用延迟初始化函数来暴露API。
const ref = useRef(null);
const getterRef = useRef(() => {
if (ref.current === null) {
ref.current = new SomeImperativeThing();
}
return ref.current;
});
useEffect(() => {
return () => {
if (ref.current !== null) {
ref.current.destroy();
ref.current = null;
}
};
}, []);
只能运行一次的Effect可以使用ref
不需要清理的Effect——甚至挂载Effect——可能不需要任何更改就可以使用新的语义。
useEffect(() => {
SomeTrackingAPI.logImpression();
}, []);
这种效果是为了记录用户看到了特定内容。如果内容被隐藏,然后再次显示,该怎么办?它应该记录第二印象吗?(如果切换选项卡重新安装视图,这就是今天的效果。)如果可以的话,我们根本不需要改变效果。如果我们只想让效果记录一次,我们可以使用ref。
在这种情况下,无论我们是挂载还是重新挂载,我们使用ref,这样我们只记录一次。
# 07. 新Hooks
# userId
useId是一个新的钩子,用于在客户端和服务器上生成唯一的ID,同时避免不匹配。它主要用于与需要唯一ID的可访问性API集成的组件库
提示
useId不用于在列表中生成密钥。密钥应该由您的数据生成。
# useTransition
使用Transition和startTransition可以将某些状态更新标记为不紧急。默认情况下,其他状态更新被视为紧急状态。React将允许紧急状态更新(例如,更新文本输入)中断非紧急状态更新(例如,呈现搜索结果列表)
- 将函数中的内容过渡,类比useCallback,将函数中的内容进行过渡
- startTransition中触发的更新会让更高优先级(如外面的click)的更新先进行
- startTransition中的延迟更新,不会触发Suspens组件的fallback,便于用户在更新期间的交互
// isPending: 过渡任务状态,true代表过渡中,false过渡结束
// startTransition: 执行的过渡任务
const [isPending , startTransition] = useTransition()
startTransition(()=>{
// ...
})
# useDeferredValue
- 允许用户推迟屏幕更新优先级不高部分 如果说某些渲染比较消耗性能,比如存在实时计算和反馈,我们可以使用这个Hook降低其计算的优先级,使得避免整个应用变得卡顿
import { useDeferredValue } from 'react';
const deferredValue = useDeferredValue(value, { timeoutMs: <some value> });
此命令设置值在timeoutMs中设置的时间后“滞后”。用户界面是必须立即更新还是必须等待数据,该命令使用户界面保持激活状态和响应性,该Hook避免了UI卡顿,并始终保持用户界面响应,以保持获取数据滞后的较小成本。
过渡单个状态值,让状态滞后变化,类比useMemo, 对值进行过渡
避开紧急任务的渲染,让出优先级
- 如果当前渲染是一个紧急更新的结果,比如用户输入,React将返回之前的值,然后在紧急渲染完成后渲染新的值
- React将在其他工作完成后立即进行更新(而不是等待任意的时间),并且像startTransition一样,延迟值可以挂起,而不会触发现有内容的意外回退。
# useTransition/useDeferredValue 区别
react 将更新分为两种,urgent update(紧急更新 )和transition update(过渡更新),useTransition/useDeferredValue都可以实现非紧急更新的延迟更新
相同点:
- useDeferredValue本质上和内部实现与useTransition一样都是标记成了过渡更新任务。
不同点:
useDeferredValue在本质上就在Effect上执行的,而useEffect内部逻辑是异步执行的,所以它在一定程度上更滞后于useTransition
startTransition 的回调函数是同步执行的,会比 useDeferredValue 执行的时机更早
- 在startTransition之中任何更新,都会标记上transition,React将在更新的时候,判断这个标记来决定是否完成此次更新。
- 所以transition可以理解成比setTimeout更早的更新。
- 但是同时要保证ui的正常响应,在性能好的设备上,transition两次更新的延迟会很小,但是在慢的设备上,延时会很大,但是不会影响UI的响应。
# 与防抖的区别:
- startTransition 和 useDeferredValue 与 setTimeout(防抖节流)的区别在于 不需要等待特定时间,可以监听任务的工作状态,当urgent update更新完毕,将会自动执行 transition update
# 08. library hooks
以下钩子是为库作者提供的,用于将库深入集成到React模型中,通常不在应用程序代码中使用。
# useSyncExternalStore
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
useSyncExternalStore是一个用于读取和订阅外部数据源的钩子,其方式与选择性hydration和时间切片等并发渲染功能兼容。
此方法返回存储的值并接受三个参数:
- subscribe:注册一个回调函数,每当存储发生更改时调用该函数
- getSnapshot:返回存储的当前值函数。
- getServerSnapshot:返回服务器渲染期间使用快照的函数。
- 最基本的例子就是订阅整个store
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
- 也可以订阅特定字段:
const selectedField = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().selectedField,
);
- 在服务器渲染时,必须序列化服务器上使用的存储值,并将其提供给useSyncExternalStore。React将在hydration过程中使用此快照,以防止服务器不匹配:
const selectedField = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().selectedField,
() => INITIAL_SERVER_SNAPSHOT.selectedField,
);
提示
[getSnapshot]必须返回一个缓存值。如果连续多次调用 getSnapshot,则它必须返回相同的确切值,除非两者之间有存储更新。 提供了一个shim,用于支持作为use-sync-external-store/shim发布的多个版本.此 shim 将useSyncExternalStore在可用时优先使用, 不可用时回退到user-space 实现
# useInsertionEffect
useInsertionEffect(didUpdate);
签名与useEffect相同,但它在所有DOM mutations(React执行对DOM的更新阶段)之前同步激发。在useLayoutEffect中读取布局之前,使用此选项将样式注入DOM。因为这个钩子的作用域有限,所以这个钩子不能访问refs,也不能调度更新。
提示
seInsertionEffect应仅限于 css-in-js 库作者。首选useEffect或useLayoutEffect。