一文梳理 React 18 新特性

本文概述了React 18的重要变化,包括异步模式(AsyncMode)、并发模式(ConcurrentMode)的深化——ConcurrentRender,自动批处理优化,以及服务器端渲染(SSR)中的Suspense组件。新特性旨在提升性能和用户体验,如自动合并更新、延迟渲染和优化SSR流程。
摘要由CSDN通过智能技术生成

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),以最小内联<script>标签标记此 HTML 的位置。等待服务器上组件的数据准备好后,React 再将剩余的 HTML发送到同一个流中。

2、hydration 的过程是逐步的,不需要等待所有的 js 加载完毕再开始 hydration,避免了页面的卡顿。

3、React 会提前监听页面上交互事件(如鼠标的点击),对发生交互的区域优先级进行 hydration。

https://github.com/reactwg/react-18/discussions/37

useSyncExternalStore

这个 API 可以防止在 concurrent 模式下,任务中断后第三方 store 被修改,恢复任务时出现tearing从而数据不一致问题。用户一般很少使用,大多情况下提供给像 Redux 这样的状态管理库使用,通过 useSyncExternalStore 可以使 React 在 concurrent mode 下,保持自身 state 和来自 Redux 的状态同步。

import * as React from 'react'// 基础用法,getSnapshot 返回一个缓存的值
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot)// 根据数据字段,使用内联的 getSnapshot 返回缓存的数据
const selectedField = React.useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField)
  • 第一个参数是一个订阅函数,订阅触发时会引起该组件的更新。
  • 第二个函数返回一个 immutable 快照, 返回值是我们想要订阅的数据,只有数据发生变化时才需要重新渲染。

useInsertionEffect

这个 hook 对现有的专为 React 设计的 css-in-js 库有着很大的作用,可以动态生成新规则与<style>标签一起插入到文档中。

假设现在我们要插入一段 css ,并且将这个操作放在渲染期间去执行。

function css(rule) {
  if (!isInserted.has(rule)) {
    isInserted.add(rule)
    document.head.appendChild(getStyleForRule(rule))
  }
  return rule
}
function Component() {
  return <div className={css('...')} />
}

这样会导致每次修改 css 样式时,react 需要在渲染的每一帧中对所有的节点重新计算所有 CSS 规则,这并不是我们想要的结果。

那我们是不是可以在所有 DOM 生成前就插入这些 css 样式,此时我们可能会想到useLayoutEffect ,但 useLayoutEffect 中可以访问 DOM,如果在这个 hook 中访问了某个 DOM 的布局样式(比如clientWidth),这样会导致我们读取的信息是错误的。

useLayoutEffect ( ( )  =>  { 
  if  ( ref.current.clientWidth  <  100 )  { 
    setCollapsed ( true ) ; 
  } 
} ) ;

useInsertionEffect 可以帮助我们避免上述问题 ,既可以满足在所有 DOM 生成前插入并且不访问 DOM。其工作原理大致与 useLayoutEffect 相同,只是此时没法访问 DOM节点的引用。我们可以在这个 hook 中插入全局的DOM节点,比如如<style> ,或SVG<defs>

const useCSS: React.FC = (rule) => {
  useInsertionEffect(() => {
    if (!isInserted.has(rule)) {
      isInserted.add(rule)
      document.head.appendChild(getStyleForRule(rule))
    }
  })
  return rule
}
const Component: React.FC = () => {
  let className = useCSS(rule)
  return <div className={className} />
}

https://github.com/reactwg/react-18/discussions/110

useId

React 一直在向着 SSR 的领域发展,但 SSR 渲染必须保证客户端与服务端生成的HTML结构相匹配。我们平时使用的如Math.random()在SSR 面前是没法保证客户端与服务端之间的 id 唯一性。

React为了解决这个问题,提出来useOpaqueIdentifier这个hook, 不过它在不同环境会产生不同的结果.

  • 在服务端会生成一个字符串
  • 在客户端会生成一个对象,必须直接传递给DOM属性

这样一来,在客户端如果需要生成多个标识,就需要调多次这个hook,因为它不支持转化为字符串,就无法使用字符串拼接。

const App: React.FC = () => {
  const tabIdOne = React.unstable_useOpaqueIdentifier();
  const panelIdOne = React.unstable_useOpaqueIdentifier();
  const tabIdTwo = React.unstable_useOpaqueIdentifier();
  const panelIdTwo = React.unstable_useOpaqueIdentifier();return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={tabIdOne} value="one">
            One
          </Tab>
          <Tab id={tabIdTwo} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={panelIdOne} value="one">
          Content One
        </TabPanel>
        <TabPanel id={panelIdTwo} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  );
}

useId 可以生成客户端与服务端之间的唯一id ,并且返回一个字符串。这样一个组件可以只需调用一次useId ,并将其结果作为整个组件所需的标识符基础(比如拼接不同的字符串),以便生成唯一 id

const App: React.FC = () => {
  const id = React.useId()
  return (
    <React.Fragment>
      <Tabs defaultValue="one">
        <div role="tablist">
          <Tab id={`${id}tab1`} value="one">
            One
          </Tab>
          <Tab id={`${id}tab2`} value="one">
            One
          </Tab>
        </div>
        <TabPanel id={`${id}panel1`} value="one">
          Content One
        </TabPanel>
        <TabPanel id={`${id}panel2`} value="two">
          Content Two
        </TabPanel>
      </Tabs>
    </React.Fragment>
  )
}

useDefferdValue

React 可以通过 useDefferdValue 允许变量延时更新,同时接受一个可选的延迟更新的最大值。React 将尝试尽快更新延迟值,如果在给定的 timeoutMs 期限内未能完成,它将强制更新。

const defferValue = useDeferredValue(value, { timeoutMs: 1000 })

useDefferdValue 能够很好的展现并发渲染时优先级调整的特性,可以用于延迟计算逻辑比较复杂的状态,让其他组件优先渲染I,等待这个状态更新完毕之后再渲染。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值