React 18 带给我们的惊喜

1、前言

React 18 的 alpha 版已经发布有段时间了,之前学习后由于没有开发实践结合去思考,对 React 18 的意义认识并不深刻。前段时间做了一些老旧项目迁移,发现复杂项目下每次渲染都要精心调整,否则就会有麻烦的性能或体验瑕疵,而 React 内部渲染顺序和优先级很难调整,就导致总体体验差了点意思。回顾了 React 18 的三个新特性,有种久旱逢甘雨的欣喜。

团队内部推行了 React hook,好处就不在这里赘述了,也陆续收到了一些负面反馈。其一就是 React hook 更加趋向面向数据实体进行拆分,而一个动作需要多个数据实体协作,例如一个 Modal Form 需要 visible 和 data 两个数据项协作,但是这两个数据项的变更会触发两次渲染结算,增加性能开销。

作者之前遇到过复杂 Form 表单下,初次渲染由于数据项过于复杂导致无限次 render 的 bug。在这个 case 中,核心的冲突就是在数据项复杂度提升的同时,React Diff 的性能就遇到了“伪瓶颈”。这里不是说 React Diff 性能差,仅仅想表达它的高性能需要更高的设计理念和实践经验,这也是相对于 Vue 等更加易学的框架而言,总的来说上限高下限也低。而 React 18 的变化让我看到了 React 团队正在关注这一部分,并且给予了更好的解决方案。

闲聊到此为止,进入正题,给大家介绍下 React 18 的四个重要新特性:

  • Automatic batching

  • Concurrent APIS

  • SSR for Suspense

  • New Render API

2、Automatic batching

在 React 中使用 setState 来进行 dispatch 组件 State 变化,当 setState 在组件被调用后,并不会立即触发重新渲染。React 会执行全部事件处理函数,然后触发一个单独的 re-render,合并所有更新。这里举个简单例子:

const [count, setCount] = useState(0);

function increment() {
  // setCount(count + 1)
  // 使用无状态函数进行优化,避免多次 re-render
  setCount(c => c + 1);
}

function handleClick() {
  increment();
  increment();
  increment();
}

最终 React 会将更新函数放到一个队列里,然后合并队列触发 setCount (3) 的 re-render,这就是 batching 的含义。

这样既可以减少程序数据状态存在中间值导致的不稳定性,也可以提升渲染性能。但是可惜的是在 React 18 之前,如果在回调函数的异步调用中,执行 setState,由于丢失了上下文,无法做合并处理,所以每次 setState 调用都会触发一次 re-render。

function handleClick() {
  // React 18 以前的版本
  (/*...*/).then(() => {
    setCount((c) => c + 1); // 立刻重渲染
    setFlag((f) => !f); // 立刻重渲染
  });
}

而 React 18 带来变化便是,任何情况下都可以合并渲染了!如果你希望在 React 18 的 setState 后立即执行重新渲染, 只需要使用 flushSync 包裹即可。

function handleClick() {
  // React 18+
  fetch(/*...*/).then(() => {
    ReactDOM.flushSync(() => {
      setCount((c) => c + 1); // 立刻重渲染
      setFlag((f) => !f); // 立刻重渲染
    });
  });
}

回归到实际开发中,Automatic batching 机制让我们有能力对渲染顺序和节奏进行一些基础的把控。例如在 Canvas 画布编辑场景中,我们可以加载完主节点框架之后立刻进行渲染,而每个节点的内容则可以进行合并渲染,尽可能加快用户看到可编辑页面的时间,同时避免 http 异步函数引起的频繁渲染的性能开销。

React 18 带给我们的惊喜_第1张图片

3、Concurrent APIS

在官方视频中明确指出了 React 18 中并不存在 Concurrent Mode,只有用于并发渲染的并发新特性。开发者希望能够在 Web Platform 引入并发渲染,来实现多个渲染任务的并行渲染,其中 Suspense 就是基于此诞生的。

React 18 提供了三个新的 API 支持这一并发特性,分别是:

  • startTransition()

  • useDeferredValue()

  • useTransition()

由于 useTransition 的官方文档并未放出来,这里就仅仅介绍另外两种 API。

3.1 startTransition()

import { startTransition } from "react";

// 紧急更新:
setInputValue(input);

// 标记回调函数内的更新为非紧急更新:
startTransition(() => {
  setSearchQuery(input);
});

简单来说,被 startTransition 包裹的 setState 触发的渲染被标记为不紧急渲染,意味着他们可以被其他紧急渲染所抢占。这种渲染优先级的调整手段可以帮助我们解决各种性能伪瓶颈,提升用户体验。

3.2 useDeferredValue()

这个 hook 适用于设置延迟值,参考官方演示视频来看。

function Page() {
  const [filters, mergeFilter] = useMergeState(defaultFilters);
  const deferedFilters = React.useDeferedValue(filters);
  return (
    
      
      
    
  );
}

useDeferedValue () 会将 List 组件的渲染变得更加平滑,深层次来看则是 defered value 引起的渲染则会被标记为不紧急渲染,会被 filters 引起的渲染进行抢占,进而达到用户快速输入搜索等场景下页面抖动或者卡顿问题。

4、SSR for Suspense

早在 2018 年,React 就推出了 Suspense 的基础版本。

React 18 带给我们的惊喜_第2张图片

它可以在客户端动态加载代码(React.lazy),配合 Suspense 组件实现数据拉取和状态控制的关注点分离(当子组件未加载完成时,父组件填充 fallback 声明的组件),但是并不能在服务器端进行加载。

}>
  
  }>
    
  

React 的开发者对 Suspense 的期望并不仅仅止步于此,他们认为 Suspense 拓展了我们对组件的概念。在 React 18 中,Suspense 可以运行在服务器端,Server Rendering 的性能不需要受制于性能最差的组件(木桶效应)。

在 React 18 之前,Server Rendering 的流程是服务器端请求所有数据,然后发送 HTML 到客户端或者说浏览器,然后由客户端的 hydrate 内容,每个环节必须按部就班的执行。当 Suspense 可以在服务器端使用之后,一旦某个组件加载慢,就可以将 fallback 的内容传输到客户端(例如下图中的 loading 态),保证用户尽可能早的可进行交互。

React 18 带给我们的惊喜_第3张图片

更加优秀的部分则是,hydrate 是可以通过用户的行为来调整优先级的,例如上图中 Profile 组件和正在 Loading 的评论组件同时处于 Suspense 的流程中,此时用户点击评论组件,React 将会优先 hydrate 评论组件,尽可能优先满足用户交互体验。

回归到代码实现细节,整体框架上服务器和客户端的连接必然趋向于持续性的长链接,因此 res.send 需要变成 res.socket,pipeToNodeWritable 替换 renderToString 并且配合 Suspense 即可(官方例子)。

5、New Render API

新的更加友好的语义化 render 方式。

const container = document.getElementById("app");

// 旧 render API
ReactDOM.render(, container);

// 新 createRoot API
const root = ReactDOM.createRoot(container);
root.render();

Client 端提供了新 水合 Hydrate API。

const root = ReactDOM.hydrateRoot(container, );

以及 新 useId () API 来为组件生成唯一 ID。

由于 Suspense 和 并发渲染在 React 18 的大规模使用,一些具有 External stores 的 API,比如全局变量、document 对象如何在并发场景下保证一致性呢?如果无法保证一致性,在并发渲染过程中可能会导致组件展示的不一致。

React 18 带给我们的惊喜_第4张图片

为了解决这个问题,React 18 提供了 useSyncExternalStore() 这个 hook,来保证获取 External stores 的一致性。

useSyncExternalStore(
  // 注册回调函数
  subscribe: (callback) => Unsubscribe,
  // 获取快照函数
  getSnapshot: () => state
) => state

具体使用方式参考:

React 18 带给我们的惊喜_第5张图片

6、React 未来展望

在官方视频中,开发者们也对未来版本的内容进行介绍。

Support for Data Fetching API

由于 Suspense 的大规模应用,其数据获取变得更加定制化,目前常见的有 Relay、React Query 等。React 官方也希望将这一部分纳入到 React 的 API 中。

Server Component

组件不仅可以通过网络读取数据、也可以后台数据层直接读取服务数据,将大大减少服务器端向客户端传输的代码量,和同构模式十分类似。

React 18 in React Native

2022 年 React 18 将和 React Native 一起发布,跨平台构建的史诗级更新,RN 并发的一些老大难将得到解决。

7、结语

结合起来看,React 18 关注点在于更快的性能、用户交互响应效率和跨平台构建,其设计理念处处包含了中断与抢占概念。React 18 给我们提供了一些从应用构建视角下的手段,例如:

  • 在 Client 端随时中断的框架设计,第一优先级渲染用户最关注的 UI 交互模块。

  • 从后端到前端 “顺滑” 的管道式 SSR,并将 hydration 过程按需化,且支持被更高优先级用户交互行为打断,第一优先水合用户正在交互的部分。

作为一名前端开发,十分期待 React 18 的到来。

 - END -

React 18 带给我们的惊喜_第6张图片

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

React 18 带给我们的惊喜_第7张图片

你可能感兴趣的:(python,java,vue,编程语言,javascript)