React 进阶 - 渲染调优

2023-05-17 20:47:27 浏览数 (1)

# 异步渲染

Suspense 是 React 提出的一种同步的代码来实现异步操作的方案。Suspense 让组件‘等待’异步操作,异步请求结束后在进行组件的渲染,即异步渲染。

Suspense 是组件,有一个 fallback 属性,用来代替当 Suspense 处于 loading 状态下渲染的内容,Suspensechildren 就是异步组件。多个异步组件可以用 Suspense 嵌套使用。

代码语言:javascript复制
/* 子组件 */

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 处于数据加载状态下,展示 Suspensefallback 的内容。

异步渲染相比传统数据交互相比:

  • 传统模式:挂载组件 -> 请求数据 -> 再渲染组件
  • 异步模式:请求数据 -> 渲染组件
  • 异步渲染好处
    • 不再需要 componentDidMountuseEffect 配合做数据交互,也不会因为数据交互后,改变 state 而产生的二次更新作用
    • 代码更加简洁, 逻辑更加清晰

# 动态加载(懒加载)

Suspense 配合 React.lazy 可以实现动态加载功能:

  • React.lazy 接受一个函数,这个函数需要动态调用 import()
  • 它必须返回一个 Promise ,该 Promise 需要 resolve 一个 default export 的 React 组件
代码语言:javascript复制
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.lazySuspense 实现动态加载原理:

整个 render 过程都是同步执行一气呵成的,但是在 Suspense 异步组件情况下允许调用 Render => 发现异步请求 => 悬停,等待异步请求完毕 => 再次渲染展示数据

  • Suspense 原理
    • Suspense 在执行内部可以通过 try{}catch{} 方式捕获异常,这个异常通常是一个 Promise ,可以在这个 Promise 中进行数据请求工作,Suspense 内部会处理这个 PromisePromise 结束后,Suspense 会再一次重新 render 把数据渲染出来,达到异步渲染的效果
  • React.lazy 原理
    • lazy 内部模拟一个 promiseA 规范场景
    • 完全可以理解 React.lazyPromise 模拟了一个请求数据的过程,但是请求的结果不是数据,而是一个动态的组件。下一次渲染就直接渲染这个组件,所以是 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 增加了 componentDidCatchstatic getDerivedStateFromError() 两个额外的生命周期,去挽救由于渲染阶段出现问题造成 UI 界面无法显示的情况。

# componentDidCatch

componentDidCatch 可以捕获异常,它接受两个参数:

  • error —— 抛出的错误
  • info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息

componentDidCatch 中可以再次触发 setState,来降级 UI 渲染,componentDidCatch() 会在 commit 阶段被调用,因此允许执行副作用。

代码语言:javascript复制
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,作为渲染使用。

代码语言:javascript复制
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
  • 因为数据本质是用闭包缓存的,所以绑定需要在在组件内部,这样才能保证每次父组件挂载,都会重新请求数据,另外也防止内存泄漏情况发生
  • 数据源更新维护困难

0 人点赞