回顾React
[toc]
# React异步更新与更新函数
React的更新是异步进行的,也就是说,当你在连续的更改state时(或者说是在UI更新之前连续更改state时),就会触发React的批处理异步更新.
例如:
const [number, setNumber] = useState(0); ...... <button onClick={() => { setNumber(number + 5); setNumber(number + 10); }} > 增加数字 </button> ......
从上面的代码可以看出,在按钮点击之后会连续更改state,这就会触发批处理异步更新.
setNumber(number + 5)
: 执行时,number为0,所以更新后应该number是5. 但是此时还没有UI更新,因此React会延迟该值的更新,所以当前的number仍旧是0.
setNumber(number + 10)
: 同理,由于上一步的延迟更新,原本number为0,执行完该句应该为10,但是延迟更新,该number值仍然为0;
":"
:UI更新,此时number为最后一次更新的值10.
更新函数:
setNumber(n => n + 1)
:该更新函数的作用就是可以根据上一次更新的值进行更新.
例如:
const [number, setNumber] = useState(0); ...... <button onClick={() => { setNumber((n)=>n+5); setNumber((n)=>n+10); }} > 增加数字 </button> ......
从上面的代码可以看出:
setNumber((n)=>n+5)
: 执行时,会将number更新为5,当然,React仍旧会延迟该值的更新,所以此刻的number值仍旧为0;
setNumber((n)=>n+10)
: 执行时,会根据上一次更新的值进行更新,上一次更新的值是5,所以此刻计算获得的值是15.当然,由于异步更新与批更新的机制,此刻的number仍旧为0.
":"
:UI更新,此时number为最后一次更新的值15.
再看一个例子:
const [number, setNumber] = useState(0); ...... <button onClick={() => { setNumber(number + 5); setNumber((n) => n + 1); setNumber(number + 10); }} > 增加数字 </button> ......
从上面的代码可以看出:
setNumber(number + 5)
: 执行时,会将number更新为5,当然,React仍旧会延迟该值的更新,所以此刻的number值仍旧为0;
setNumber((n) => n + 1)
:执行时,会根据上一次更新的值进行更新,上一次更新的值是5,所以此刻计算获得的值是15.当然,由于异步更新与批更新的机制,此刻的number仍旧为0.
setNumber(number + 10)
:执行时,会将number更新为10,因为此刻的number值为0,0+10=10,他并不会根据上一次值的更新而更新,当然,React仍旧会延迟该值的更新,所以此刻的number值仍旧为0;
":"
:UI更新,此时number为最后一次更新的值10.
总结
因此,React出于对性能的考虑, 提供了异步更新与批更新的机制.
更新函数与普通的更新可以看作如下区别:
更新函数: setState((n)=>n);
普通更新: setState(10);
其实普通更新等价于: setState(()=>10); 只不过他的更新函数没有传入上一次更新的值(n).
对于一个如下的代码,其可以表示为:
const [number, setNumber] = useState(0); ...... <button onClick={() => { setNumber(number + 5); setNumber((n) => n + 1); setNumber(number + 10); setNumber((n) => n + 2); setNumber((n) => n + number); }} > ADD </button> ......
在下一次渲染期间,React 会遍历 state 队列:
更新队列(初始number=0) | n(上一次的返回值) | 返回值 | 本次执行的代码 |
---|---|---|---|
() => number + 5 | 0 (未使用) | 5 | setNumber(number + 5) |
(n) => n + 1 | 5 | 5 + 1 = 6 | setNumber((n) => n + 1) |
() => number + 10 | 6 (未使用) | 10 | setNumber(number + 10) |
(n) => n + 2 | 10 | 12 | setNumber((n) => n + 2) |
(n) => n + number | 12 | 12 | setNumber((n) => n + number) |
最终结果为12.
const [number, setNumber] = useState(0);
const anotherNumber = number;
此刻,anotherNumber也是响应式的.
命名惯例
通常可以通过相应 state 变量的第一个字母来命名更新函数的参数:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
如果你喜欢更冗长的代码,另一个常见的惯例是重复使用完整的 state 变量名称,如 setEnabled(enabled => !enabled)
,或使用前缀,如 setEnabled(prevEnabled => !prevEnabled)
。
# 更新state中的对象
state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。
虽然严格来说 React state 中存放的对象是可变的,但你应该像处理数字、布尔值、字符串一样将它们视为不可变的。因此你应该替换它们的值,而不是对它们进行修改。
换句话说,你应该 把所有存放在 state 中的 JavaScript 对象都视为只读的。
所以,如果只是更新对象中的某个属性,可以采用展开语法复制对象:
setPerson({ firstName: e.target.value, // 从 input 中获取新的 first name lastName: person.lastName, email: person.email });
更改为:
setPerson({ ...person, // 复制上一个 person 中的所有字段 firstName: e.target.value // 但是覆盖 firstName 字段 });
当然,对于更改嵌套对象的属性,则需要:
setPerson({ ...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据 city: 'New Delhi' // 但是将 city 的值替换为 New Delhi! } });
**使用 Immer 编写简洁的更新逻辑 **
使用:
- 运行
npm install use-immer
添加 Immer 依赖 - 用
import { useImmer } from 'use-immer'
替换掉import { useState } from 'react'
import { useImmer } from 'use-immer'; const [person, updatePerson] = useImmer({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { updatePerson(draft => { draft.name = e.target.value; }); }
由 Immer 提供的 draft
是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!从原理上说,Immer 会弄清楚 draft
对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。
# 更新state中的数组
数组是另外一种可以存储在 state 中的 JavaScript 对象,它虽然是可变的,但是却应该被视为不可变。同对象一样,当你想要更新存储于 state 中的数组时,你需要创建一个新的数组(或者创建一份已有数组的拷贝值),并使用新数组设置 state。
避免使用 (会改变原始数组) | 推荐使用 (会返回一个新数组) | |
---|---|---|
添加元素 | push ,unshift | concat ,[...arr] 展开语法 |
删除元素 | pop ,shift ,splice | filter ,slice |
替换元素 | splice ,arr[i] = ... 赋值 | map |
排序 | reverse ,sort | 先将数组复制一份 |
或者,你可以使用 Immer ,这样你便可以使用表格中的所有方法了。
# 组件的保留与重置
只要一个组件还被渲染在 UI 树的相同位置,React 就会保留它的 state。 如果它被移除,或者一个不同的组件被渲染在相同的位置,那么 React 就会丢掉它的 state。
相同位置的相同组件会使得 state 被保留下来. 对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!
React 不知道你的函数里是如何进行条件判断的,它只会“看到”你返回的树。你可以认为它们有相同的“地址”:根组件的第一个子组件的第一个子组件。不管你的逻辑是怎么组织的,这就是 React 在前后两次渲染之间将它们进行匹配的方式。
由于切换时,是两个相同的组件进行切换,因此切换时不会组件重置.
<div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <div> <Counter isFancy={false} /> </div> )} </div>
但是! 当你在不同位置渲染即使相同组件时,组件仍旧会被重置,(即使第一个已被销毁,但他实际位置仍旧为1).
<div> {isFancy && ( <div> <Counter isFancy={true} /> </div> ) } {!isFancy && ( <div> <Counter isFancy={false} /> </div> )} </div>
但是! 当你在相同位置渲染不同的组件时,组件的整个子树都会被重置。
<div> {isFancy ? ( <div> <Counter isFancy={true} /> </div> ) : ( <section> <Counter isFancy={false} /> </section> )} </div>
由于div 和 section不一样,因此里面的Counter会在切换时销毁.
# 使用reducer整合状态逻辑
当一个state被频繁的不同状态的更改,且逻辑复杂时,便可以考虑使用reducer整合状态逻辑.
使用 reducers 管理状态与直接设置状态略有不同。它不是通过设置状态来告诉 React “要做什么”,而是通过事件处理程序 dispatch 一个 “action” 来指明 “用户刚刚做了什么”。(而状态更新逻辑则保存在其他地方!)因此,我们不再通过事件处理器直接 “设置 task”,而是 dispatch 一个 “添加/修改/删除任务” 的 action。这更加符合用户的思维。
action 对象可以有多种结构。
按照惯例,我们通常会添加一个字符串类型的
type
字段来描述发生了什么,并通过其它字段传递额外的信息。type
是特定于组件的.选一个能描述清楚发生的事件的名字!dispatch({ // 针对特定的组件 type: 'what_happened', // 其它字段放这里 });
$ 编写一个Reducer函数:
reducer 函数就是你放置状态逻辑的地方。它接受两个参数,分别为当前 state 和 action 对象,并且返回的是更新后的 state:
例如,对于一个需要增删改查的表单,其reducer函数应该如此写:
taskReducer.js
:
export default const tastReducer = (state,action) => { switch (action.type) { case 'added': { return [ ...state, { id: action.id, text: action.text, done: false, }, ]; } case 'changed': { return state.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return state.filter((t) => t.id !== action.id); } default: { throw Error('未知 action: ' + action.type); } } }
在每一个case里面都要将更新后的state return出去.
$ 在组件中使用reducer
App.js
import { useReducer } from 'react'; import tasksReducer from './tasksReducer.js'; export default function TaskApp() { //用useReducer替换useState //const [tasks, setSasks] = useState(initialTasks); const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task, }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId, }); } return ( <> <h1>布拉格的行程安排</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); }
当然,在reducer里面对对象和数组如果觉得一直需要...tasks比较麻烦,我们仍旧可以使用 Immer
这个库来简化 reducer
.
useImmerReducer
让你可以通过 push
或 arr[i] =
来修改 state.
function tasksReducer(draft, action) { switch (action.type) { case 'added': { draft.push({ id: action.id, text: action.text, done: false, }); break; } case 'changed': { const index = draft.findIndex((t) => t.id === action.task.id); draft[index] = action.task; break; } case 'deleted': { return draft.filter((t) => t.id !== action.id); } default: { throw Error('未知 action:' + action.type); } } }
# React中的Vue-Slot
React也可以实现类似Vue中Slot的功能,具体就是:
... <Card> <Avatar /> <Avatar /> </Card> ...
那么,在Card这个组件中,便可以获得Avatar这个组件作为props.
Card.tsx:
export default function Card({ children }) { return ( <section className="section"> {children} </section> ); }
此时,children就为那两个Avatar组件.(不止可以传组件,也可以传普通DOM元素).
当然,如果有其他的props,需要将其他props放在children前面.
//{ ...otherProps,children }为结构语法, 原本prop = { ...otherProps,children }; export default function Card({ ...otherProps,children }){ ... }
# Props透传 -- Context
context允许父组件向其下层无论多深的任何组件提供信息,并且无需通过props进行显示传递.
Context 可以让父节点,甚至是很远的父节点都可以为其内部的整个组件树提供数据。
如何实现props透传?
-
创建一个context.
levelContext.js
:import { createContext } from "react"; export const LevelContetext = createContext(1);
其中,创建的这个1就表示 levelContext 这个context的默认值.
-
在指定数据的组件中提供这个context. 我们需要用 context provider 将子组件进行包裹起来以提供levelContext.
propvideData.jsx
:import { levelContext } from "./levelContext.js" export default function ProvideData() { const nowLevel = 100; return ( <div> <LevelContetext.Provider value={nowLevel}> <NeedData></NeedData> </LevelContetext.Provider> </div> ) }
这段代码告诉React,如果ProvideData组件中的任意层次的子组件请求levelContext这个context数据,那么就将value = {nowLevel} 中的nowLevel返回给他们.
组件会使用 UI 树中在它上层最近的那个
<LevelContext.Provider>
传递过来的值。 -
在需要数据的组件内使用刚刚创建的context.
needData.jsx
:import { useContext } from "react"; import { LevelContetext } from "./levelContext.js" export default function NeedData() { const level = useContext(LevelContetext); } switch (level) { case 1:... case 2:... case 3:... }
# React中也有Ref
我们使用useState时,当state值更新了的时候,其当前组件和其子组件都会进行更新.但是ref允许更新数据的同时不进行新的渲染.
如何使用?
import { useRef } from "react"; const numberRef = useRef(0); //此时打印numberRef的值会是: console.log(numberRef) => { current:0 }
可以通过 numberRef.current
来访问当前值,同时也可以直接进行修改而无需通过 setState.
但是,由于你改变了该值后,并不会触发渲染,因此,如果你将ref值搬到html视图中,改变该值并不会体现到页面中.(因为他不会触发渲染.)
何时使用ref?
- 存储timeoutID
- 获取页面中的dom元素.
- ...
以下代码是将ref放入HTML元素上时的情况.
import { useRef } from "react" export default const App(){ const domRef = useRef(null); //然后就可以使用任意dom的API domRef.current.scrollIntoView(); console.log(domRef.current); //返回null,因为此时dom元素还未挂载. return ( <> <div ref={domRef}></div> </> ) }
以下代码是将ref放入自己创建的组件上时的情况.
import { useRef } from "react"; export default const App() { const domRef = useRef(null); return ( <div> <Fake ref={domRef} /> </div> )
当你直接访问放到自定义组件上的ref时,控制台会报错:
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
发生这种情况是因为默认情况下,React 不允许组件访问其他组件的 DOM 节点。甚至自己的子组件也不行!这是故意的。Refs 是一个应急方案,应该谨慎使用。手动操作 另一个组件的 DOM 节点会使你的代码更加脆弱。
但是,如果你真的需要这样做,请参考:
想要暴露其 DOM 节点的组件必须选择该行为。一个组件可以指定将它的 ref “转发”给一个子组件。使用 forwardRef
API:
const Fake = forwardRef((props, ref) => { //注意此处是(props,ref)而不是{props,ref}. return <input {...props} ref={ref} />; });
它是这样工作的:
<Fake ref={domRef} />
告诉 React 将对应的 DOM 节点放入domRef.current
中。但是,这取决于Fake
组件是否允许这种行为, 默认情况下是不允许的。Fake
组件是使用forwardRef
声明的。 这让从上面接收的domRef
作为第二个参数ref
传入组件,第一个参数是props
。Fake
组件将自己接收到的ref
传递给它内部的<input>
。- 现在,你可以访问了.
当然,如果你想让子组件只暴露一部分内容而不是全部,那么可以使用 useImperativeHandle
做到这一点.
在 Fake 组件中:
const Fake = forwardRef(function Fake(props, ref) => { //注意此处是(props,ref)而不是{props,ref}. const realRef = useRef(null); useImperativeHandle(ref, () => ({ // 只暴露 focus,没有别的 focus() { realInputRef.current.focus(); }, })); return <input {...props} ref={realRef} />; });
# Effect
什么是Effect:
Effect 通常用于暂时“跳出” React 代码并与一些 外部 系统进行同步。这包括浏览器 API、第三方小部件,以及网络等等。
如何编写Effect:
-
声明Effect
import { useEffect } from "react"; function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { // 每次渲染后都会执行此处的代码 console.log(count); }); return <div />; }
每当你的组件渲染时,React 将更新屏幕,然后运行
useEffect
中的代码。换句话说,useEffect
会把这段代码放到屏幕更新渲染之后执行.const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); }); //此代码会造成死循环.
-
指定Effect依赖
-
Effect的依赖必须是响应式的.
-
一般来说,Effect 会在 每次 渲染时执行。但更多时候,并不需要每次渲染的时候都执行 Effect。将 依赖数组 传入
useEffect
的第二个参数,以告诉 React 跳过不必要地重新运行 Effect。 -
例如,在声明Effect的第一段程序内,可以在useEffect的第二个参数添加**[count]**,这表示只有是由于count引起的渲染时,才会触发useEffect.
-
当然,如果你只想让Effect里的代码只执行一遍,那么可以第二个参数添加**[]空数组,那么该组件的Effect在组件挂载**后执行.
挂载
组件挂载主要是指React的自定义组件被使用并显示在页面上.当离开该页面后,组件自动被卸载,再次进入该页面将再次被挂载.
< 在开发环境中,React 会在初始挂载组件后,立即再挂载一次,用于帮助开发者解决未及时清理
useEffect(() => { // 这里的代码会在每次渲染后执行 }); useEffect(() => { // 这里的代码只会在组件挂载后执行 }, []); useEffect(() => { //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行 }, [a, b]);
-
-
按需添加清理函数
useEffect(() => { const connection = createConnection(); connection.connect(); return () => { connection.disconnect(); }; }, []);
useEffect return出去的函数就是清理函数. **每次重新执行 Effect 之前,**React 都会调用清理函数;组件被卸载时,也会调用清理函数。
Effect的生命周期
首先,我们先来看看 每个 React 组件的生命周期.
- 当组件被添加到屏幕上时,它会进行组件的 挂载。
- 当组件接收到新的 props 或 state 时,通常是作为对交互的响应,它会进行组件的 更新。
- 当组件从屏幕上移除时,它会进行组件的 卸载。
组件的生命周期并不适用于Effect.
Effect 能够在需要时始终具备启动和停止的弹性 =>
-
Effect 在当前组件挂载完毕后,组件更新时将执行清理函数并启动新一轮的effect.
-
Effect 的更新仅与依赖项有关,与组件自己的更新无关(除非组件卸载又挂载).
-
如果没有设置依赖项,将会与组件的更新同步.
-
如果依赖项为[],那么仅在组件挂载完毕之后执行一次,组件更新也不会触发effect的更新.
一个组件里 Effect 可以写多个,每个 Effect 都代表一个独立的同步过程.
# 如何编写自定义Hook
...
# React 实用Hook
has introduce:
useState()
: 用于创建响应式的数据. [has]useRef()
: 用于创建不会随着组件的更新而重置的数据. [has]useContext()
: 用于Props穿透. [has]useEffect()
: 允许组件连接到外部系统并与之同步. [has]useReducer()
: ... [has]
other:
-
useCallback:
允许在多次渲染中缓存函数的Hook. [性能优化Hook]- 用法:
const cachedFn = useCallback(fn, dependencies)
; - 参数:
fn
:想要缓存的函数,React将会在初次渲染时返回(而非调用)该函数. 当下一次渲染时,如果 dependencies 相比于上一次渲染没有发生改变,那么将会返回相同的函数,否则将会返回最新一次渲染时返回的函数,并将其缓存以便之后使用.dependencies
: 和Effect的dependencies一样.
- 返回值: 初次渲染时返回fn函数,在之后的渲染中,如果依赖没有发生改变,则返回上一次缓存的函数,否则就返回这一次渲染返回的函数. 简而言之,
useCallback
在多次渲染中缓存一个函数,直至这个函数的依赖发生改变。
例如:
function ProductPage({ productId, referrer, theme }) { // 每当 theme 改变时,都会生成一个不同的函数 function handleSubmit(orderDetails) { post('/product/' + productId + '/buy', { referrer, orderDetails, }); } return ( <div className={theme}> {/* 这将导致 ShippingForm props 永远都不会是相同的,并且每次它都会重新渲染 */} {/*ShippingForm 里面内置了memo,可以在dependencies保持不刷新*/} <ShippingForm onSubmit={handleSubmit} /> </div> ); }
- 用法:
-
useMemo
: 它在每次重新渲染的时候能够缓存计算的结果。-
用法:
const catch = useMemo(calculateValue, dependencies)
-
参数:
calculateValue
:要缓存计算值的函数,他应该是一个没有任何参数的纯函数,React将会在首次渲染的时候调用(与useCallBack的不同点)该函数,并将调用的结果进行缓存,如果后面dependencies没有发生变化,则将直接返回缓存结果;否则将重新调用并缓存结果.dependencies
: ...
-
返回值: 在初次渲染时,
useMemo
返回不带参数调用calculateValue
的结果。在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用
calculateValue
,并返回最新结果。 -
另外,memo还可以跳过组件的渲染.例如:
export default function TodoList({ todos, tab, theme }) { // ... return ( <div className={theme}> <List items={visibleTodos} /> </div> ); } //List组件将会在theme变更后重新渲染,即使他与上一次渲染后的结果一致.
你可以通过将它包装在
memo
中,这样当它的 props 跟上一次渲染相同的时候它就会跳过本次渲染:import { memo } from 'react'; const List = memo(function List({ items }) { // ... });
useMemo
经常与useCallback
一同出现。
-
-
useDeferredValue
: 允许延迟更新UI的某些部分.-
用法:
const deferredValue = useDeferredValue(value)
-
参数: value: 要延迟的值.
-
返回值: 在组件的初始渲染期间,返回的延迟值将与你提供的值相同。但是在组件更新时,React 将会先尝试使用旧值进行重新渲染(因此它将返回旧值),然后再在后台使用新值进行另一个重新渲染(这时它将返回更新后的值)。这个后台重新渲染是可以被中断的,如果
value
有新的更新,React 会从头开始重新启动后台渲染。
-
-
useId
: 生成唯一ID .但是 不应该被用来生成列表中的 key. -
useImperativeHandle
: 自定义由 ref 暴露出来的句柄。-
用法:
useImperativeHandle(ref, createHandle, dependencies?)
-
参数:
ref
:该ref
是你从forwardRef
渲染函数 中获得的第二个参数。createHandle
:该函数无需参数,它返回你想要暴露的 ref 的句柄。该句柄可以包含任何类型。通常,你会返回一个包含你想暴露的方法的对象。dependencies
: 可选参数.
-
事例:
import { forwardRef, useImperativeHandle } from 'react'; const MyInput = forwardRef(function MyInput(props, ref) { useImperativeHandle(ref, () => { return { // ... 你的方法 ... }; }, []); // ...
-
-
useTransition
: 在不阻塞 UI 的情况下更新状态的 React Hook。-
用法:
const [isPending, startTransition] = useTransition()
-
参数: 无参.
-
返回值:
-
isPending
: 告诉你是否存在待处理的 transition。 -
startTransition
: 函数,使用此方法将状态更新标记为 transition。例如:
function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState('about'); function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); } // …… }
-
-
transition 可以使用户界面的更新在慢速设备上仍保持响应性。
通过 transition,UI 仍将在重新渲染过程中保持响应性。例如用户点击一个选项卡,但改变了主意并点击另一个选项卡,他们可以在不等待第一个重新渲染完成的情况下完成操作。
-
# 可用的其他组件
<Suspense> : 允许在子组件完全加载完成之前展示备选方案.
-用法:
<Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>
-参数:
children
: 真正的 UI 渲染内容。如果children
在渲染中被挂起,Suspense 边界将会渲染fallback
。fallback
: 真正的 UI 未渲染完成时代替其渲染的备用 UI,它可以是任何有效的 React 节点。
默认情况下,Suspense 内部的整棵组件树都被视为一个单独的单元。例如,即使 只有一个 组件因等待数据而被挂起,Suspense 内部的整棵组件树中的 所有 的组件都将被替换为加载中指示器. 然后,当它们都准备好展示时,它们将一起出现。
嵌套加载Suspense:
<Suspense fallback={<BigSpinner />}> <Biography /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums /> </Panel> </Suspense> </Suspense>
加载序列将会是:
- 如果
Biography
没有加载完成,BigSpinner
会显示在整个内容区域的位置。 - 一旦
Biography
加载完成,BigSpinner
会被内容替换。 - 如果
Albums
没有加载完成,AlbumsGlimmer
会显示在Albums
和它的父级Panel
的位置。 - 最后,一旦
Albums
加载完成,它会替换AlbumsGlimmer
。
注意!
延迟值和 transition 都可以让你避免显示 Suspense 后备方案,而是使用内联指示器。transition 将整个更新标记为非紧急的,因此它们通常由框架和路由库用于导航。另一方面,延迟值在你希望将 UI 的一部分标记为非紧急,并让它“落后于” UI 的其余部分时非常有用。
# 懒加载
lazy
能够让你在组件第一次被渲染之前延迟加载组件的代码。
import { lazy } from 'react'; const MarkdownPreview = lazy(() => import('./MarkdownPreview.js'));