# 异步渲染
Suspense
是 React 提出的一种同步的代码来实现异步操作的方案。Suspense
让组件‘等待’异步操作,异步请求结束后在进行组件的渲染,即异步渲染。
Suspense
是组件,有一个 fallback
属性,用来代替当 Suspense
处于 loading
状态下渲染的内容,Suspense
的 children
就是异步组件。多个异步组件可以用 Suspense
嵌套使用。
/* 子组件 */
function UserInfo() {
const user = getUserInfo()
return <h1>{user.name}</h1>
}
/* 父组件 */
export default function Index() {
return (
<Suspense fallback={<div>loading...</div>}>
<UserInfo />
</Suspense>
)
}
Suspense
包裹异步渲染组件 UserInfo
,当 UserInfo
处于数据加载状态下,展示 Suspense
中 fallback
的内容。
异步渲染相比传统数据交互相比:
- 传统模式:挂载组件 -> 请求数据 -> 再渲染组件
- 异步模式:请求数据 -> 渲染组件
- 异步渲染好处
- 不再需要
componentDidMount
或useEffect
配合做数据交互,也不会因为数据交互后,改变state
而产生的二次更新作用 - 代码更加简洁, 逻辑更加清晰
- 不再需要
# 动态加载(懒加载)
Suspense
配合 React.lazy
可以实现动态加载功能:
React.lazy
接受一个函数,这个函数需要动态调用import()
- 它必须返回一个
Promise
,该Promise
需要resolve
一个default export
的 React 组件
const LazyComponent = React.lazy(() => import("./Component"))
基本使用:
代码语言:javascript复制const LazyComponent = React.lazy(() => import("./Component"))
export default function Index() {
return (
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
)
}
用 React.lazy
动态引入 Component
里面的组件,配合 Suspense
实现动态加载组件效果。这样很利于代码分割,不会让初始化的时候加载大量的文件。
# 实现原理
React.lazy
和 Suspense
实现动态加载原理:
整个 render 过程都是同步执行一气呵成的,但是在 Suspense
异步组件情况下允许调用 Render => 发现异步请求 => 悬停,等待异步请求完毕 => 再次渲染展示数据。
Suspense
原理Suspense
在执行内部可以通过try{}catch{}
方式捕获异常,这个异常通常是一个Promise
,可以在这个Promise
中进行数据请求工作,Suspense
内部会处理这个Promise
,Promise
结束后,Suspense
会再一次重新render
把数据渲染出来,达到异步渲染的效果
React.lazy
原理lazy
内部模拟一个 promiseA 规范场景- 完全可以理解
React.lazy
用Promise
模拟了一个请求数据的过程,但是请求的结果不是数据,而是一个动态的组件。下一次渲染就直接渲染这个组件,所以是React.lazy
利用Suspense
接收Promise
,执行Promise
,然后再渲染这个特性做到动态加载的
# 渲染错误边界
React 组件渲染过程如果有一个环节出现问题,就会导致整个组件渲染失败,那么整个组件的 UI 层都会显示不出来,这样造成的危害是巨大的,如果越靠近 APP 应用的根组件,渲染过程中出现问题造成的影响就越大,有可能直接造成白屏的情况。
代码语言:javascript复制function ErrorTest() {
return
}
function Test() {
return <div>This is Test Component</div>
}
class Index extends React.Component {
componentDidCatch(...arg) {
console.log("componentDidCatch", ...arg)
}
render() {
return (
<div>
<ErrorTest />
<div>Just for test componentDidCatch</div>
<Test />
</div>
)
}
}
由于 ErrorTest
不是一个真正的组件但是却用来渲染,结果会造成整个 Index
组件渲染异常,Test
也会受到牵连,UI 都不能正常显示。
为了防止如上的渲染异常情况 React 增加了 componentDidCatch
和 static getDerivedStateFromError()
两个额外的生命周期,去挽救由于渲染阶段出现问题造成 UI 界面无法显示的情况。
# componentDidCatch
componentDidCatch
可以捕获异常,它接受两个参数:
error
—— 抛出的错误info
—— 带有componentStack
key 的对象,其中包含有关组件引发错误的栈信息
componentDidCatch
中可以再次触发 setState
,来降级 UI 渲染,componentDidCatch()
会在 commit
阶段被调用,因此允许执行副作用。
class Index extends React.Component {
state = {
hasError: false,
}
componentDidCatch(...arg) {
/* report error */
uploadErrorLog(...arg)
this.setState({
hasError: true,
})
}
render() {
const { hasError } = this.state
return (
<div>
{hasError ? <div>There is something wrong</div> : <ErrorTest />}
<div>Just for test componentDidCatch</div>
<Test />
</div>
)
}
}
# static getDerivedStateFromError
React 更期望用 getDerivedStateFromError
代替 componentDidCatch
用于处理渲染异常的情况。getDerivedStateFromError
是静态方法,内部不能调用 setState
。
getDerivedStateFromError
返回的值可以合并到 state
,作为渲染使用。
class Index extends React.Component {
state = {
hasError: false,
}
static getDerivedStateFromError() {
return {
hasError: true,
}
}
render() {
const { hasError } = this.state
return (
<div>
{hasError ? <div>There is something wrong</div> : <ErrorTest />}
<div>Just for test componentDidCatch</div>
<Test />
</div>
)
}
}
注意事项: 如果存在 getDerivedStateFromError
生命周期钩子,那么将不需要 componentDidCatch
生命周期再降级 UI 。
# key 的合理使用
合理的使用 key
有助于能精准的找到用于新节点复用的老节点。
# 异步组件
实现效果
- 异步请求数据,请求完数据再挂载组件
- 没有加载完数据显示 loading 效果
- 可量化生产
思路
- 可以使用
React.lazy
实现动态加载,那么可以先请求数据,然后再加载组件,这时候以props
形式将数据传递给目标组件,实现异步效果
实现
代码语言:javascript复制function AsyncComponent(Component, api) {
const AsyncComponentPromise = () =>
new Promise(async (resolve, reject) => {
const data = await api()
resolve({
default: (props) => <Component rdata={data} {...props} />,
})
})
return React.lazy(AsyncComponentPromise)
}
- 用
AysncComponent
作为一个 HOC 包装组件,接受两个参数,第一个参数为当前组件,第二个参数为请求数据的 api - 声明一个函数给
React.lazy
作为回调函数,React.lazy
要求这个函数必须是返回一个Promise
。在Promise
里面通过调用 api 请求数据,然后根据返回来的数据rdata
渲染组件,别忘了接受并传递props
使用
代码语言:javascript复制const getData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "cell",
say: "hello",
})
}, 1000)
})
}
function Test({ rdata, age }) {
const { name, say } = rdata
console.log("Test render")
return (
<div>
<div>name: {name}</div>
<div>age: {age}</div>
<div>say: {say}</div>
</div>
)
}
export default class Index extends React.Component {
LazyTest = AsyncComponent(Test, getData)
render() {
const { LazyTest } = this
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyTest age={18} />
</Suspense>
</div>
)
}
}
注意点
- 需要约定好接受数据格式
rdata
和数据交互形式api
- 因为数据本质是用闭包缓存的,所以绑定需要在在组件内部,这样才能保证每次父组件挂载,都会重新请求数据,另外也防止内存泄漏情况发生
- 数据源更新维护困难