一文梳理 React 18 新特性

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),以最小内联

你可能感兴趣的:(React,react.js,javascript,前端)