前言 {#前言}
React 没法做到 JS 做不到的事,其任务调度也逃不开浏览器的事件循环。
相比起写 Vue,React 更需要开发者知道浏览器本身的各种特性。
缓存 {#缓存}
一个父组件重新渲染时,它的所有后代组件也会重新渲染,即使它们的 props、state 等状态都没有变化。
区分 diff 算法的作用:
diff 算法的作用范围是虚拟 DOM,它不会阻止组件的函数被重新执行,它只负责比较新旧虚拟 DOM 树的差异,并且只会对有差异的部分更新真实 DOM。
当新一轮的渲染逻辑执行完毕后,会生成新的虚拟 DOM 树,并通过 diff 算法与上一轮旧的虚拟 DOM 树进行比较,找出差异,然后最小更新真实 DOM。
React 提供了一些优化手段,可以避免不必要的重新执行渲染逻辑,缓存组件、函数、计算结果等。
React.memo 缓存组件 {#React-memo-缓存组件}
memo 允许组件在 props 没有改变的情况下跳过重新渲染。
使用 memo 将组件包装起来,以获得该组件的一个记忆化版本。通常情况下,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。但 React 仍可能会重新渲染它:记忆化是一种性能优化,而非保证。
|-----------------|--------------------------------------------------------------------------------------------|
| 1 2 3 4
| import { memo } from "react"; export default memo(function Child() { // ...... });
|
可以传入第二个参数,一个比较函数 arePropsEqual
,接受两个参数:新旧props,用于自定义比较 props 的逻辑,返回 true
表示 props 没有改变。默认情况下,React 将使用 Object.is
比较每个 prop。
父组件
|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| export default function Parent() { const [, forceUpdate] = useState(0); console.log("Parent fn"); useInsertionEffect(() => { console.log("Parent insertionEffect"); }); useLayoutEffect(() => { console.log("Parent layoutEffect"); }); useEffect(() => { console.log("Parent rendered"); }); const data = "Child"; return ( <div> Parent <button onClick={() => { forceUpdate((prev) => prev + 1); }} > 父组件重新渲染 </button> <Child data={data}></Child> </div> ); }
|
子组件
|---------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13
| export default memo(function Child({ data }: { data: string }) { console.log("Child fn"); useInsertionEffect(() => { console.log("Child insertionEffect"); }); useLayoutEffect(() => { console.log("Child layoutEffect"); }); useEffect(() => { console.log("Child rendered"); }); return <div>{data}</div>; });
|
父组件重新渲染时,子组件将不再重新渲染(组件函数不被执行),所以子组件的 Effect 也不会被调用。
|-----------------|------------------------------------------------------------------------------|
| 1 2 3 4
| Parent fn Parent insertionEffect Parent layoutEffect Parent rendered
|
useMemo 缓存计算结果 {#useMemo-缓存计算结果}
如果一个值可以基于现有的 props 或 state 计算得出,不要把它作为一个 state,而是在渲染期间直接计算这个值。如果这个计算量较大,可以使用 useMemo 缓存计算结果。
|-----------|-------------------------------------------------------------------|
| 1
| const cachedValue = useMemo(calculateValue, dependencies)
|
将上一节的 data 改为一个对象再传递给子组件。
|-----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7
| // 父组件 const data = { title: "Child" }; <Child data={data}></Child> // 子组件 export default memo(function Child({ data }: { data: { title: string } }) { return <div>{data.title}</div>; });
|
再触发父组件重新渲染时,记忆化后的子组件也会重新渲染,因为 props 传递的是一个对象,每次父组件函数重新执行都会生成一个新的对象,导致 props 发生变化。
简单的无需计算得出的对象可以使用 useRef
保证引用不变。
|-------------|-----------------------------------------------------------------------------|
| 1 2
| const dataRef = useRef({ title: "Child" }); // 现在重新渲染父组件时,子组件不会重新渲染
|
但有时候数据是需要计算得出的,特别是有响应式值参与计算时,就可以使用 useMemo
缓存计算结果。和 Effect 一样,第二个参数是依赖项数组,只有依赖项发生变化时才会重新计算。
|-----------|---------------------------------------------------------------|
| 1
| const data = useMemo(() => ({ title: "title" }), []);
|
useMemo 经常与 memo 相配合,保证组件的记忆化,而不需要手动深比较 props。
useCallback 缓存函数 {#useCallback-缓存函数}
组件的内部函数在每次渲染时都会被重新创建,useCallback 可以缓存函数,避免不必要的重新创建。
|-----------|--------------------------------------------------------|
| 1
| const cachedFn = useCallback(fn, dependencies)
|
和 useMemo 其实差不多,只是 useCallback 缓存的是函数,useMemo 缓存的是任意值(也可以是函数)。
非阻塞更新 state {#非阻塞更新-state}
有些时候,某些 state 的更新并不需要立即生效,也就是不需要立刻触发重新渲染,React 提供了一些 API 来实现非阻塞更新 state。让开发者可以控制更新的优先级,提高用户体验。
useDeferredValue {#useDeferredValue}
useDeferredValue 接受一个值,返回一个延迟更新的值。通常传入一个 state,可以延迟更新 state。
在初始渲染期间,返回的延迟值 与你提供的值 相同。在更新期间,延迟值 会滞后于 最新的值,React 首先会在不更新延迟值的情况下进行重新渲染,然后在后台尝试使用新接收到的值进行重新渲染。
下面是一个搜索的例子,用户在输入期间,通常不要求立即更新搜索结果,可以使用 useDeferredValue
延迟更新搜索结果,直到用户停止输入。试着一直按住键盘不放,搜索结果不会立即更新,而是等待用户停止输入后再更新。
一个搜索的例子
|------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { useState, useDeferredValue, memo } from "react"; export default function SearchComponent() { const [text, setText] = useState(""); const deferredText = useDeferredValue(text); return ( <> <input value={text} onChange={(e) => setText(e.target.value)} /> <SlowList text={deferredText} /> </> ); } // 使用memo缓存SlowList组件,只有在text变化时才会重新渲染 const SlowList = memo(function SlowList({ text }: { text: string }) { const items = Array.from({ length: 200 }, (_, i) => ( <SlowItem key={i} text={text} /> )); return <ul className="items">{items}</ul>; }); function SlowItem({ text }: { text: string }) { const startTime = performance.now(); while (performance.now() - startTime < 1) { // 每个 item 暂停 1ms,模拟极其缓慢的代码 } return <li className="item">Text: {text}</li>; }
|
不要遗忘了
memo
,否则每次父组件重新渲染时,SlowList 也会重新渲染,达不到优化的目的。
当迫切的任务执行后,再得到新的状态,触发重新渲染。
通过 useDeferredValue
你可以控制视图的更新优先级,让用户体验更加流畅。
被推迟的"后台"渲染是可中断的。例如,如果你再次在输入框中输入,React 将会中断渲染,并从新值开始重新渲染。React 总是使用最新提供的值。
如果 React 正在重新渲染一个大型列表,但用户进行了另一次键盘输入,React 会放弃该重新渲染,先处理键盘输入,然后再次开始在后台渲染。详见:延迟一个值与防抖和节流之间有什么不同?
让 React 可以切分任务 {#让-React-可以切分任务}
React 是通过调度器来调度任务的,为了好理解,可以将一个任务单元理解为一次组件函数的调用。而 React 时间切片的最小单位也是组件函数,你应该保证单个组件函数的执行时间尽可能短,以便 React 执行单个任务时,能够在一帧内完成,不造成页面卡顿。
时间切片的最小单位是组件函数:你没有办法打断一个 JS 函数的执行,或者在一个 JS 函数执行时动态插入其它函数的执行。即使是 Generator 函数,你也只能控制它每一段的执行时机,而不能在执行栈中间让出。 错误的模拟
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7
| const SlowComponent = function () { const startTime = performance.now(); while (performance.now() - startTime < 1000) { // 模拟极其缓慢的代码 } return <div>SlowComponent</div>; };
|
上面的 SlowComponent 组件函数执行时间过长,React 的调度器不会起作用,该函数进入到执行栈后,一切都晚了,页面将会老老实实被阻塞 1s。 正确的模拟,将 1s 耗时的任务,分散为 1000 个 1ms 的任务
|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const SlowComponent = function SlowList() { const items = Array.from({ length: 1000 }, (_, i) => ( <SlowItem key={i} text={i.toString()} /> )); return <ul className="items">{items}</ul>; }; function SlowItem({ text }: { text: string }) { const startTime = performance.now(); while (performance.now() - startTime < 1) { // 每个 item 暂停 1ms,模拟极其缓慢的代码 } return <li className="item">Text: {text}</li>; }
|
React 没法做到 JS 做不到的事,其任务调度也逃不开浏览器的事件循环
React 将组件函数的调用作为宏任务,而宏任务本身是不会阻塞浏览器渲染的,React 通过调度器在一帧内尽可能多的执行宏任务,如果单个宏任务被拿到执行栈中执行耗时过长,那么就会阻塞浏览器渲染、后续任务的执行,造成页面卡顿。所以,尽可能写出 React 可以切分的任务。
涉及到 React Fiber 的内容了,这些底层的具体实现以后再说吧。
useTransition {#useTransition}
useTransition 用于将状态更新标记为非阻塞的 Transition,使得开发者可以控制状态更新的优先级,确保用户交互不被阻塞,适用于复杂或耗时的操作。
标记为 Transition 的状态更新可以被其他状态更新打断
|-----------|---------------------------------------------------------------|
| 1
| const [isPending, startTransition] = useTransition();
|
isPending 是一个布尔值,表示是否存在待处理的 transition。通常用于做一些指示效果。
startTransition
接受一个回调函数,将其中的状态更新标记为 transition,该回调函数会在后台执行,不会阻塞当前渲染。
**与 useDeferredValue 的区别:**useDeferredValue 产生一个延迟更新的副本,绑定了该副本的 UI 会在后台延迟更新,而 useTransition 是直接将该状态更新标记为非阻塞的 transition,降低了优先级,不会产生副本。所以 useDeferredValue 可以用于处理用户输入,而 useTransition 不能。 不应将控制输入框的状态变量标记为 transition
|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10
| const [text, setText] = useState(''); // ... function handleChange(e) { // ❌ 不应将受控输入框的状态变量标记为 Transition startTransition(() => { setText(e.target.value); }); } // ... return <input value={text} onChange={handleChange} />;
|
例子 {#例子}
下面是一个 Tab 切换的选项卡的例子,其中渲染第二个选项卡是耗时的。
|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { memo, useState } from "react"; export default function Tab() { const [tab, setTab] = useState(0); function switchTab(tab: number) { setTab(tab); } return ( <div> <button onClick={() => switchTab(0)}>栏一</button> <button onClick={() => switchTab(1)}>栏二</button> <button onClick={() => switchTab(2)}>栏三</button> <TabContent tab={tab}></TabContent> </div> ); } const TabContent = memo(function ({ tab }: { tab: number }) { return ( <div> {tab === 0 && <div>栏一内容</div>} {tab === 1 && <SlowComponent></SlowComponent>} {tab === 2 && <div>栏三内容</div>} </div> ); });
|
当点击栏二后,整个应用都被阻塞了 1s,用户体验很差。可以使用 useTransition
来优化。
现在,点击栏二后,栏二的内容不会立即显示,而是在后台渲染,且不会阻塞用户的操作,如果用户在栏二渲染完成前切换到其他栏,那么栏二的内容渲染会被取消。
|-------------------|--------------------------------------------------------------------------------------|
| 1 2 3 4 5
| function switchTab(tab: number) { startTransition(() => { setTab(tab); }); }
|
startTransition {#startTransition}
可以直接从 react 中导入 startTransition
函数使用(如果你用不到 isPending 的话),效果是一样的。
|-----------|--------------------------------------------------|
| 1
| import { startTransition } from "react";
|
lazy 组件懒加载 {#lazy-组件懒加载}
lazy 函数可以让你懒加载一个组件,只有在组件首次渲染时才会加载组件代码(按需加载)。
|-------------|-------------------------------------------------------------------------------------------------------|
| 1 2
| import { lazy } from 'react'; const LazyComponent = lazy(() => import('./LazyComponent.js'));
|
load 函数:
lazy 接受一个 load
函数,该函数需要返回一个 Promise
,resolve 后,React 将结果 .default
渲染为 React 组件。如果 reject ,则 React 将抛出 reason 给最近的错误边界处理。
返回的 Promise 和 Promise 的解析值都将被缓存 ,因此 React 不会多次调用 load 函数。
使用位置:
应该在模块顶层使用 lazy,不要在组件内部使用,否则每次组件重新渲染时都会重新加载子组件代码,导致状态重置。
|-----------------|------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4
| function App() { // ? Bad: 这将导致在重新渲染时重置所有状态 const LazyComponent = lazy(() => import('./LazyComponent.js')); }
|
lazy 组件通常和 Suspense 组件一起使用,以指示子组件正在加载中。
Suspense 后备方案组件 {#Suspense-后备方案组件}
Suspense 组件可以指示 React 在等待加载异步组件时渲染一些 fallback
(后备方案) 内容。
React 将展示后备方案直到 children 需要的所有代码 和数据都加载完成。
|---------------|-------------------------------------------------------------------------|
| 1 2 3
| <Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>
|
只有启用了 Suspense 的数据源才会激活 Suspense 组件,包括:
- 支持 Suspense 的框架如 Relay 和 Next.js。
- 使用 lazy 懒加载组件代码。
- 使用 use 读取 Promise 的值。
Suspense 无法检测在 Effect 或事件处理程序中获取数据的情况。
配合 lazy 使用: Loading.tsx
|-----------------------|--------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7
| export default function Loading() { return ( <p> <i>Loading...</i> </p> ); }
|
dangerouslySetInnerHTML 和 innerHTML 作用一样,但名称用于警告开发者,这个属性是危险的,可能导致 XSS 攻击,应该手动对插入的内容进行过滤。 MarkdownPreview.tsx
|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12
| import { marked } from "marked"; import DOMPurify from "dompurify"; export default function MarkdownPreview({ markdown }: { markdown: string }) { const sanitizedHTML = DOMPurify.sanitize(marked.parse(markdown) as string); return ( <div className="md-content" dangerouslySetInnerHTML={{ __html: sanitizedHTML }} /> ); }
|
首次点击显示预览时,会显示 Loading...,2s 后显示 markdown 的预览,并能看到网络请求了相关组件文件,往后多次点击显示预览时,不会再加载组件文件,因为已经缓存了。 LazyComponent.tsx
|------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| import { Suspense, useState, lazy, ComponentType } from "react"; import Loading from "../Markdown/Loading"; // 懒加载 MarkdownPreview 组件 const MarkdownPreview = lazy(() => delayForComponent(import("../Markdown/MarkdownPreview")) ); export default function LazyComponent() { const [markdown, setMarkdown] = useState("Hello, **world**!"); const [showPreview, setShowPreview] = useState(false); return ( <div> <label> <p>输入 Markdown:</p> <textarea value={markdown} onChange={(e) => setMarkdown(e.target.value)} /> <input type="checkbox" checked={showPreview} onChange={(e) => setShowPreview(e.target.checked)} /> 显示预览 </label> {showPreview && ( // 使用 Suspense 包裹懒加载的组件 <Suspense fallback={<Loading />}> <h2>预览</h2> <MarkdownPreview markdown={markdown}></MarkdownPreview> </Suspense> )} </div> ); } // 模拟组件延迟加载 function delayForComponent(promise: Promise<{ default: ComponentType<any> }>) { return new Promise((resolve) => { setTimeout(resolve, 2000); }).then(() => promise); }
|
use 读取Promise值(Canary) {#use-读取Promise值-Canary}
use 函数用于读取类似于 Promise 或 Context 的值。这是一个预发布的 API。
读取 Context:
不同于 useContext
,use 可以在条件语句和循环中调用,更加灵活。
|-----------------|-----------------------------------------------------------------------------------------|
| 1 2 3 4
| if (show) { const theme = use(ThemeContext); return <hr className={theme} />; }
|
读取 Promise:
使用 use 可以让组件在异步数据还未准备好时暂停渲染,等数据准备好后再继续渲染,从而避免显示不完整的 UI。
如果没有 use,你可能会这样写:通过 useState 设置一个状态,然后在 useEffect 中读取 Promise 的值。 使用 useState 和 useEffect 读取 Promise 的值
|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default function UseTest() { const [message, setMessage] = useState<string | null>(null); useEffect(() => { fetchMessage().then((message) => setMessage(message)); }, []); return <div>{message ?? "Loading..."}</div>; } // 模拟请求数据 function fetchMessage(): Promise<string> { return new Promise((resolve) => setTimeout(resolve, 1000, "Hello, world!")); }
|
与传统的异步数据加载(使用 useEffect + useState)不同,use 函数让你无需管理这些状态,直接返回异步结果,代码更加简洁。 使用 use 读取 Promise 的值
|---------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| export default function UseTest() { // 还是需要一个状态来保存 Promise const [messagePromise, setMessagePromise] = useState<Promise<string>>(); function postMessage() { setMessagePromise(fetchMessage()); } return ( <> <button onClick={postMessage}>请求消息</button> {messagePromise && ( <Suspense fallback={<p>⌛清求中...</p>}> <Message messagePromise={messagePromise!} /> </Suspense> )} </> ); } function Message({ messagePromise }: { messagePromise: Promise<string> }) { // 使用 use 函数处理 Promise,获取数据 const messageContent = use(messagePromise); return <p>{messageContent}</p>; } // 模拟请求数据 function fetchMessage(): Promise<string> { return new Promise((resolve) => setTimeout(resolve, 2000, "Hello, world!")); }
|
实现 use 函数 {#实现-use-函数}
首先需要知道,单独使用 use、lazy 是会报错的,必须配合 Suspense 使用。
|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2
| Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
|
use、lazy 等钩子在渲染过程中可能会抛出一个 Promise 来表示数据尚未准备好,而 Suspense 会 catch 捕获该 promise 来管理异步流程。
- 组件挂起:当一个组件在渲染过程中需要等待某些异步操作完成(如数据获取),它会抛出一个 Promise。
- 捕获挂起:Suspense 会捕获到这个被抛出的 Promise。此时,React 知道该组件还未准备好渲染,需要等待异步操作完成。
- 显示备用内容:查找最近的 Suspense 组件,并显示其 fallback 属性中定义的备用内容。
- 异步操作完成:一旦 Promise 完成,React 会重新尝试渲染挂起的组件。这时,组件所需的数据或资源已经准备好,可以正常渲染。
这里我们不关心 Suspense 的实现,它可能是在 .then 中告诉 React 重新渲染挂起的组件。
思路很清楚,在组件首次渲染时给 promise 加上 pending 状态标识,然后调用 then 方法,监听状态改变、保存结果,状态改变后,React 会自动重新渲染该组件,这时 use 就能直接返回结果了。
|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function use<T = any>( promise: { status?: string; result?: T } & Promise<T> ): T { // 初始状态 status 在浏览器 Promise 中没有,所以需要自己定义 if (!promise.status) { promise.status = "pending"; } if (promise.status === "fulfilled") { // 状态是 fulfilled,直接返回数据 return promise.result!; } else if (promise.status === "rejected") { // 状态是 rejected,抛出错误 throw promise.result; } else { // 状态是 pending // 绑定 then 方法 promise.then( (result: any) => { // 成功时,设置状态为 fulfilled promise.status = "fulfilled"; // 保存数据 promise.result = result; }, (reason: any) => { // 失败时,设置状态为 rejected promise.status = "rejected"; // 保存错误 promise.result = reason; } ); // 抛出 Promise 对象 throw promise; } }
|
因为 Suspense 需要捕获 use 抛出的 promise 错误,且 React 需要在 promise 完成后重新渲染 Suspense.children,所以 use 和 对应的 Suspense 不能在同一个组件中使用,应该是父子组件的关系。
useSyncExternalStore {#useSyncExternalStore}
useSyncExternalStore 用于同步外部状态 store 到 React 组件。
参数:
subscribe
函数,用于订阅 store 的变化,返回一个取消订阅的函数。当外部 store 发生变化时,能通过该函数通知 React 重新调用 getSnapshot 并在需要的时候重新渲染组件。getSnapshot
函数,用于获取 store 的快照。当新旧快照不相同(Object.is),React 会重新渲染组件。getServerSnapshot
(可选),在服务端渲染时,React 会使用该快照作为初始值。
简单说,你需要在外部状态变化时执行 subscribe 函数传入的 callback
回调,以通知 React 通过 getSnapshot
获取最新的状态,并在新旧状态不同时重新渲染组件。
一个获取网络状态的自定义 Hook
|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { useSyncExternalStore } from "react"; export function useOnlineStatus() { const isOnline = useSyncExternalStore( subscribe, () => navigator.onLine, () => true ); return isOnline; } function subscribe(callback: () => void) { // 利用浏览器的原生 API 订阅网络状态变化,与 React 建立联系。 window.addEventListener("online", callback); window.addEventListener("offline", callback); return () => { window.removeEventListener("online", callback); window.removeEventListener("offline", callback); }; }
|
使用:
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10
| import { useOnlineStatus } from "./useOnlineStatus"; export default function Online() { const isOnline = useOnlineStatus(); return ( <div> <h2>{isOnline ? "Online" : "Offline"}</h2> </div> ); }
|
useDebugValue {#useDebugValue}
useDebugValue 用于在 React 开发者工具中为自定义 Hook 添加标签。
以 useOnlineStatus 为例,默认情况下,React 开发者工具只会显示其用到的 Hook 的标签。可以预见,当内部数据结构变得复杂后,这么显示是不够直观反应 Hook 的作用的。
|---------------|-------------------------------------------------------|
| 1 2 3
| hooks OnlineStatus: 1 SyncExternalStore: true
|
通过 useDebugValue
我们可以为其添加标签,让开发者工具更加直观。
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6
| export function useOnlineStatus() { // ... useDebugValue(isOnline ? "Online" : "Offline"); } OnlineStatus: "Online" 1 SyncExternalStore: true
|
用什么作为标签,完全取决于开发者,可以是任何值,只要能更好的理解这个 Hook 的作用。
useId {#useId}
useId 用于生成稳定 且唯一的 ID,通常用于传递给无障碍属性。
该 ID 在单个 React 渲染树内 是唯一的,也就是说在同一次渲染中,useId 生成的 ID 对于每个组件实例都是唯一的,不会重复。且多次渲染间,ID 值也是稳定和组件实例一一对应,不会变化。
|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12
| export default function Form() { const id = useId(); // 生成唯一 ID,作为前缀。 return ( <form> <label htmlFor={id + '-firstName'}>名字:</label> <input id={id + '-firstName'} type="text" /> <hr /> <label htmlFor={id + '-lastName'}>姓氏:</label> <input id={id + '-lastName'} type="text" /> </form> ); }
|