React.js学习-一文梳理 React 18 新特性(转载)

2022-05-16 22:23:312022-05-16 22:38:14

转载自: https://blog.csdn.net/qq_41257129/article/details/123371328

React 的迭代过程

React 从 v16 到 v18 主打的特性包括三个变化:

  • v16: Async Mode (异步模式)
  • v17: Concurrent Mode (并发模式)
  • v18: Concurrent Render (并发更新)

React 中 Fiber 树的更新流程分为两个阶段 render 阶段和 commit 阶段。组件的 render 函数执行时称为 render (本次更新需要做哪些变更),纯 js 计算;而将 render 的结果渲染到页面的过程称为 commit (变更到真实的宿主环境中,在浏览器中就是操作 DOM)。

在 Sync 模式下, render 阶段是一次性执行完成;而在 Concurrent 模式下 render 阶段可以被拆解,每个时间片内执行一部分,直到执行完毕。由于 commit 阶段有 DOM 的更新,不可能让 DOM 更新到一半中断,必须一次性执行完毕。

  • Async Mode: 让 render 变为异步、可中断的。
  • Concurrent Mode : 让 commit 在用户的感知上是并发的。
  • Concurrent Render : Concurrent Mode 中包含 breaking change,比如很多库不兼容(mobx 等),所以 v18 提出了 Concurrent Render ,减少了开发者的迁移成本。

React 并发新特性

并发渲染机制(concurrent rendering)的目的:根据用户的设备性能和网速对渲染过程进行适当的调整, 保证 React 应用在长时间的渲染过程中依旧保持可交互性,避免页面出现卡顿或无响应的情况,从而提升用户体验。

v18 正式引入了的并发渲染机制,并基于此给我们带来了很多新特性。这些新特性都是可选的并发功能,使用了这些新特性的组件并能触发并发渲染,并且与其整个子树都将自动开启 strictMode

新 root API

v18 之前 root 节点对用户不透明。

import * as ReactDOM from 'react-dom'
import App from './App'const root = document.getElementById('app')
// v18 之前的方法
ReactDOM.render(<App/>,root)
v18 中我们可以通过 createRoot Api 手动创建 root 节点。

import * as ReactDOM from 'react-dom'
import App from './App'const root = ReactDOM.createRoot(document.getElementById('app'))
// v18 的新方法
root.render(<App/>,root)

想要使用 v18 中其他新特性 API, 前提是要使用新的 Root API 来创建根节点。

Automatic batching 自动批处理优化

批处理: React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setState 事件合并)

在 v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise 链、 setTimeout 等异步代码以及原生事件处理函数。

// v18 之前
function handleClick () {

  fetchSomething().then(() => {

      // React 17 及之前的版本不会批处理以下的 state:
      setCount((c) => c + 1) // 重新渲染
      setFlag((f) => !f) // 二次重新渲染
    })
}
// v18下
// 1、promise链中
function handleClick () {

  fetchSomething().then(() => {

      setCount((c) => c + 1)
      setFlag((f) => !f) // 合并为一次重新渲染
    })
}
// 2、setTimeout等异步代码中
setTimeout(() => {

  setCount((c) => c + 1)
  setFlag((f) => !f) // 合并为一次重新渲染
}, 5000)
// 3、原生事件中
element.addEventListener("click", () => {

setCount((c) => c + 1)
  setFlag((f) => !f) // 合并为一次重新渲染
})

如果想退出自动批处理立即更新的话,可以使用 ReactDOM.flushSync() 进行包裹。

import * as ReactDOM from 'react-dom'function handleClick () {

  // 立即更新
  ReactDOM.flushSync(() => {

    setCounter(c => c + 1)
  })
  // 立即更新
  ReactDOM.flushSync(() => {

    setFlag(f => !f)
  })
}

startTransition

可以用来降低渲染优先级。分别用来包裹计算量大的 functionvalue ,降低优先级,减少重复渲染次数。

举个例子:搜索引擎的关键词联想。一般来说,对于用户在输入框中输入都希望是实时更新的,如果此时联想词比较多同时也要实时更新的话,这就可能会导致用户的输入会卡顿。这样一来用户的体验会变差,这并不是我们想要的结果。

我们将这个场景的状态更新提取出来:一个是用户输入的更新;一个是联想词的更新。这个两个更新紧急程度显然前者大于后者。

以前我们可以使用防抖的操作来过滤不必要的更新,但防抖有一个弊端,当我们长时间的持续输入(时间间隔小于防抖设置的时间),页面就会长时间都不到响应。而 startTransition 可以指定 UI 的渲染优先级,哪些需要实时更新,哪些需要延迟更新。即使用户长时间输入最迟 5s 也会更新一次,官方还提供了 hook 版本的 useTransition ,接受传入一个毫秒的参数用来修改最迟更新时间,返回一个过渡期的 pending 状态和 startTransition 函数。

import * as React from "react";
import "./styles.css";
​
export default function App() {

  const [value, setValue] = React.useState();
  const [searchQuery, setSearchQuery] = React.useState([]);
  const [loading, startTransition] = React.useTransition(2000);
​
  const handleChange = (e) => {

    setValue(e.target.value);
    // 延迟更新
    startTransition(() => {

      setSearchQuery(Array(20000).fill(e.target.value));
    });
  };
​
  return (
    <div className="App">
      <input value={
    value} onChange={
    handleChange} />
      {
    loading ? (
        <p>loading...</p>
      ) : (
        searchQuery.map((item, index) => <p key={
    index}>{
    item}</p>)
      )}
    </div>
  );
}

所有在 startTransition 回调中更新的都会被认为是非紧急处理,如果一旦出现更紧急的处理(比如这里的用户输入), startTransition 就会中断之前的更新,只会渲染最新一次的状态更新。

startTransition 的原理就是利用了 React 底层的优先级调度模型。

更多例子: 真实世界示例:为慢速渲染添加 startTransition

SSR 下的 Suspense 组件

Suspense 的作用: 划分页面中需要并发渲染的部分。

hydration[水化]:ssr 时服务器输出的是字符串(html),客户端(一般是浏览器)根据这些字符串并结合加载的 JavaScript 来完成 React 的初始化工作这一阶段为水化。

React v18 之前的 SSR, 客户端必须一次性的等待 HTML 数据加载到服务器上并且等待所有 JavaScript 加载完毕之后再开始 hydration , 等待所有组件 hydration 后,才能进行交互。即整个过程需要完成从获取数据(服务器)→ 渲染到 HTML(服务器)→ 加载代码(客户端)→ 水合物(客户端)这一套流程。这样的 SSR 并不能使我们的完全可交互变快,只是提高了用户的感知静态页面内容的速度。

React v18 在 SSR 下支持了 Suspense ,最大的区别是什么呢?

  1. 服务器不需要等待被 Suspense 包裹组件是否加载到完毕,即可发送 HTML,而代替 suspense 包裹的组件是 fallback 中的内容,一般是一个占位符(spinner),以最小内联