React18 有哪些变化?

简介: 我们越来越能感受到,React的开发者把升级重点放到了「渐进升级」上,那么v18中有哪些新变化、新特性呢?
作者 | 游鹿

image.png

可能是React15到16的不兼容变更太多,开发者们升级相当痛苦,所以很长一段时间React开发者都没有再发布新版本,而是在 v16 上集成各种新能力,16.3/16.8/16.12 几乎每隔几个版本就有一颗赛艇的新特性出现。

在长达2年半的 v16 版本后,React团队发布了 v17,同时宣布这一版本的定位是一版技术改造的过渡版本,主要目标是降低后续版本的升级成本。在 v17 之前,不同版本的 React 无法混用,很重要的一个原因是之前版本中事件委托是挂在document上的,v17 开始,事件委托挂载到了渲染 React 树的根 DOM 容器中,这使多 React 版本并存成为了可能。(意味着React 17+可混用,老页面维持 v17,新页面使用v18 v19 等)

image.png

我们越来越能感受到,React的开发者把升级重点放到了「渐进升级」上,仅在v17发布了2个小版本后,v18的alpha就出现了,并且只需要用户做极小、甚至不需要改动就能让现有React APP在 v18 上工作。那么v18中有哪些新变化、新特性呢?

注:本文内容来自 reactwg/react-18 的部分discussion,笔者翻译阅读理解后写出来的。如果有理解不到位的地方,欢迎大家评论区交流、讨论、指正~

React 18

React18的升级策略是「渐进升级」,包括名声在外的并发渲染等在内的新能力都是可选的,不会立刻对组件行为带来任何明显的破坏性变化。

You can upgrade to React 18 with minimal or no changes to your application code, with a level of effort comparable to a typical major React release.

你几乎不需要对应用程序中的代码进行任何改动就可以直接升级到 React 18,并不会比以往的 React 版本升级要困难。

React官网 https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html

并发渲染是React底层的一次重要架构设计升级,并发渲染的优势在于提高React APP性能。当你使用了一些React18新特性后,你可能已经用上了并发渲染。

新的 Root API

在React18中, ReactDOM.render() 正式成为Legacy,并增加了新的RootAPI ReactDOM.createRoot() ,他们的用法差别如下:

import ReactDOM from ‘react-dom’;
import App from 'App';

ReactDOM.render(<App />, document.getElementById('root'));
import ReactDOM from ‘react-dom’;
import App from 'App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

可以看到,通过新的API,我们可以为一个React App创建多个根节点,甚至在未来可以用不同版本的React来创建。React18 保留了上述两种用法,老项目不想改仍然可以用 ReactDOM.render() ;新项目想提升性能,可以用 ReactDOM.createRoot() 借并发渲染的东风。

自动 Batching

什么是Batching

Batching is when React groups multiple state updates into a single re-render for better performance.

为了使应用获得更好的性能,React把多次的状态更新(state updates),合并到一次渲染中

React17只会把浏览器事件(如点击)发生期间的状态更新合并掉。而React18会把事件处理器发生后的状态更新也合并掉。举个例子:

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClickPrev() {
     setCount(c => c - 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }
  
  function handleClickNext() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClickPrev}>Prev</button>
      <button onClick={handleClickNext}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

什么是Automatic Batching

在React 18中,只要使用 新的 Root API ReactDOM.createRoot() 方法,就能直接享受自动batching的能力!这里列举一些自动更新的场景:

image.png

不使用automatic batching

batching 是安全的,但也存在一些特殊情况不希望batching发生,比如:你需要在状态更新后,立刻读取新DOM上的数据等。这种情况下请使用 ReactDOM.flushSync() (React官方不推荐常态化使用这一API):

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

对Hooks/Classes的影响

  • 对 Hooks 没有任何影响
  • 对 Classes 大部分情况下没影响,关注一种模式:是否在两次setState之间读取了state值。差异如下:
handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // 在 React17 及之前,打印出来是 { count: 1, flag: false }
    // 在 React18,打印出来是 { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

如果不想通过调整代码逻辑的方式进行修正,可以直接采用 ReactDOM.flushSync() :

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // 在 React18,打印出来是 { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

新的 Suspense SSR 架构

Suspense在React16/18的区别

Suspense早在React16就以试验性API的形式出来了,相比较旧版本的Legacy Suspense,新版本的Concurrent Suspense更符合用户直觉。

React官方说这两个版本存在比较小的差异,但由于新版 Suspense 的实现是基于并发渲染的,所以这仍然是一个Breaking Changes,这里介绍下差异:

<Suspense fallback={<Loading />}>
  <ComponentThatSuspends />
  <Sibling />
</Suspense>

image.png

现存SSR架构的问题

现存SSR架构原理不多解释,它的问题在于,一切都是串行的,在任一前序任务没完成之前,后一任务都无法开始,也就是“All or Nothing”,一般是如下流程:

  1. 服务器内部获取数据
  2. 服务器内部渲染 HTML
  3. 客户端从远程加载代码
  4. 客户端开始hydrate(水合)

React18新策略

React18提供了Suspense,打破了这种串行的限制,优化前端的加载速度和可交互所需等待时间。这一SSR架构依赖两个新特性:

  • 服务器端的「流式HTML」:使用API pipeToNodeWritable
  • 客户端的「选择性Hydration」:使用

新版本Suspense SSR速度更快的原理是什么呢?以下面的结构为例:

1、流式 HTML

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

如上一个页面结构,<Comments> 是通过接口异步获取的,这一过程数据请求比较慢,所以我们把它包裹在 <Suspense> 里。在普通的SSR架构里,一般只能等<Comments>加载进来之后才能进行下一环节。在新模式下,HTML流首先返回的内容里是不会有 <Comments> 组件相关HTML信息的,取而代之的是 <Spinnger> 的HTML:

<main>
  <nav>
    <!--NavBar -->
    <a href="/">Home</a>
   </nav>
  <aside>
    <!-- Sidebar -->
    <a href="/profile">Profile</a>
  </aside>
  <article>
    <!-- Post -->
    <p>Hello world</p>
  </article>
  <section id="comments-spinner">
    <!-- Spinner -->
    <img width=400 src="spinner.gif" alt="Loading..." />
  </section>
</main>

等服务端获取到了 <Comments> 的数据后,React再把后加入的 <Comments> 的HTML信息,通过同一个流(stream)发送过去,React会创建一个超小的内联 <script> 标签,把 HTML 放在“正确的位置”。

<div hidden id="comments">
  <!-- Comments -->
  <p>First comment</p>
  <p>Second comment</p>
</div>
<script>
  // This implementation is slightly simplified
  document.getElementById('sections-spinner').replaceChildren(
    document.getElementById('comments')
  );
</script>

所以与传统的 HTML 流不同,它不必按自上而下的顺序发生。

2、选择性 Hydration
代码拆分是我们常用的手段,我们可以用 React.lazy 把一部分代码从主包中拆出来。

import { lazy } from 'react';

const Comments = lazy(() => import('./Comments.js'));

// ...

<Suspense fallback={<Spinner />}>
  <Comments />
</Suspense>

在React18以前,React.lazy 不支持服务端渲染,即便是最流行的解决方案也让大家从「为了代码拆分不使用SSR」和「使用SSR但要在所有js加载完成后才能hydratie」中二选一。

而在React18版本,被 <Suspense> 包裹的子组件可以延后hydratie,这一行为是React内部自动做掉的,所以React.lazy也默认支持了SSR。

新特性startTransition

为了解决什么问题?

使用此 API 可以防止内部函数执行拖慢 UI 响应速度。

以查询选择器为例:用户输入关键词,请求远程数据并展示搜索结果。

// Urgent: Show what was typed
setInputValue(input);

// Not urgent: Show the results
setSearchQuery(input);

输入文字时用户是希望得到即时反馈的,而查询并展示结果则是允许有延迟的(事实上,开发者经常人为地用一些手段让他们延迟更新,比如debounce)

在引入 startTransition 后用法是:

import { startTransition } from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

什么是transition?

更新可以分为两类:

  • 紧急更新(Urgent Updates):比如打字、点击、拖动等,在直觉上需要立即响应的行为,如果不立即响应会给人感觉出错了;
  • 过渡更新(Transition Updates):将 UI 从一个视图过渡到另一个视图。它不需要即时响应,有点延迟是在预期范围内、可接受的。

其实在React应用中,大部分更新在概念上都应当是Transition Updates,但是出于向后兼容的角度考虑,transition是可选的,所以在React18中默认的更新方式仍然是Urgent Updates,想要使用Transition Updates可以把函数用startTransition 包裹起来。

startTransition 与 setTimeout的区别是什么?

主要有两点:

  1. startTransition更早于setTimeout处理渲染更新,这一差别在一些性能较差的机器上感知稍微明显。在运行时,startTransition与普通函数一样,都是立即执行的,只不过函数执行带来的所有update会被标记为"transition",React在处理更新时会使用这一标记作为参考。
  2. setTimeout内的大型的屏幕更新会锁定页面,在此期间用户无法与页面交互。而被标记为“transition”的更新是可被打断的,

何时使用

慢渲染:一些React需要花费大量时间的复杂渲染

慢网络:耗时的网络请求

参考文档

React文档
https://reactjs.org/blog/2021/06/08/the-plan-for-react-18.html

https://reactjs.org/blog/2020/10/20/react-v17.html

https://reactjs.org/blog/2020/08/10/react-v17-rc.html

https://reactjs.org/blog/2020/02/26/react-v16.13.0.html

React工作组讨论
https://github.com/reactwg/react-18/discussions

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

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

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

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

其他
https://zhuanlan.zhihu.com/p/379072979


image.png

相关文章
|
6月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
357 0
|
6月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
75 0
|
6月前
|
前端开发
【第31期】一文学会用React Hooks组件编写组件
【第31期】一文学会用React Hooks组件编写组件
76 0
|
6月前
|
存储 前端开发 JavaScript
【第29期】一文学会用React类组件编写组件
【第29期】一文学会用React类组件编写组件
75 0
|
6月前
|
前端开发 开发者
【第26期】一文读懂React组件编写方式
【第26期】一文读懂React组件编写方式
62 0
|
6月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
121 0
|
6月前
|
存储 前端开发 中间件
React组件间的通信
React组件间的通信
52 1
|
6月前
|
前端开发 JavaScript API
React组件生命周期
React组件生命周期
119 1
|
6月前
|
存储 前端开发 JavaScript
探索 React Hooks 的世界:如何构建出色的组件(下)
探索 React Hooks 的世界:如何构建出色的组件(下)
探索 React Hooks 的世界:如何构建出色的组件(下)
|
6月前
|
缓存 前端开发 API
探索 React Hooks 的世界:如何构建出色的组件(上)
探索 React Hooks 的世界:如何构建出色的组件(上)
探索 React Hooks 的世界:如何构建出色的组件(上)