初尝React 18新特性(上)

2,404 阅读8分钟

react 18带来了什么

我总结react18主要包括了3个主要的改动

  1. automatic batching ,也就是自动批处理来减少渲染。
  2. 新的功能 startTransition。
  3. 新的SSR架构。

之所以能实现这些功能,取决于react18新加入的concurrent rendering,也就是并发渲染的机制。这个机制主要发生在幕后,但它为 React 解锁了非常多新的可能性,来帮助你提高你应用程序的实际与感知性能。并且,react团队采取了渐进式的升级方式,也就是说,只有新特性引起的更新才会采用并发渲染。这意味着,无需重写代码即可直接使用 React 18,且可以根据自己的节奏和需要来尝试新特性,对于开发者而言,这很友好。

自动批处理

批处理是将react多个更新分组到单个重新渲染中以获取更好的性能,例如下面的例子,react只会执行一次渲染

export function Demo1(){

  const [count1, setCount1] = useState(1);
  const [count2, setCount2] = useState(2);

  const handleClick = useCallback(() => {
    setCount1(c => c + 1);  // 没有重新渲染
    setCount2(c => c + 2);  // 没有重新渲染
    // 最后react批处理
  }, []);

  return (
    <div>
      <h1>{count1}</h1>
      <h2>{count2}</h2>
      <button onClick={handleClick}>Click</button>
    </div>
  );
};

这对性能很有用,因为它避免了不必要的重新渲染。它还可以防止组件呈现仅更新一个状态变量的“半完成”状态,这可能会导致错误。但是,React 的批量更新时间并不一致。例如,如果您需要获取数据,然后更新状态,react不会批处理,而是执行两次独立的更新。这是因为 React 过去只浏览器事件(如点击)期间批量更新,但这里我们在事件已经被处理(在 fetch 回调中)之后更新状态

export function Demo1(){

  const [count1, setCount1] = useState(1);
  const [count2, setCount2] = useState(2);

  const handleClick = useCallback(() => {
    fetch(url).then(res => {
      setCount1(c => c + 1);
      setCount2(c => c + 2);
    })

  }, []);

  return (
    <div>
      <h1>{count1}</h1>
      <h2>{count2}</h2>
      <button onClick={handleClick}>Click</button>
    </div>
  );
};

在 React 18 之前,我们只在 React 事件处理程序期间批量更新。默认情况下,React 中不会对 promise、setTimeout、本机事件处理程序或任何其他事件中的更新进行批处理。

那什么是自动批处理呢

从react 18开始,react从createRoot开始,无论何处的更新,都会自动批处理,意味着任何事件内的更新将以与 React 事件内的更新相同的方式进行批处理。导致更少的渲染工作,从而在您的应用程序中获得更好的性能:

fetch(url).then(res => {
    setCount1(c => c + 1);
    setCount2(c => c + 2);
});
setTimeout(() => {
    setCount1(c => c + 1);
    setCount2(c => c + 2);
}, 2000);

像上述这种,react18都会进行自动批处理,那怎么关闭这个新特性呢?通常情况下,批处理是安全的,但某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容,对于这种,通过 ReactDOM.flushSync() 来取消批处理。

   setTimeout(() => {
      flushSync(() => {
        setCount1(c => c + 1);
      });
      flushSync(() => {
        setCount2(c => c + 2);
      });
    }, 2000);

新功能 startTransition

官方概述:在 React 18 中,我们引入了一个新的 API,即使在大屏幕更新期间,它也有助于保持您的应用程序响应。通过将特定更新标记为“转换”,此新 API 可让您显着改善用户交互。React 将让您在状态转换期间提供视觉反馈,并在转换发生时保持浏览器响应。

这解决了什么问题呢?

举个例子,我们现在有这么一个需求,在Input输入框中输入内容,我们拿到新值来搜索并且更新列表,而如果列表内容过多,可能会导致页面在呈现内容时出现延迟,这是,我们在输入框打字时交互会出现卡顿,交互感觉缓慢且无响应,即使列表不是太长,但是列表项本身也可能很复杂,每次击键时都不同,可能没有明确的方法来优化它们的呈现。

那从概念上讲,需要两次更新,一是输入框输入内容的更新,而是列表的更新。相比较来说,显然输入框的更新是更加紧急的事情。用户希望第一次更新是即时的,因为这些交互的本机浏览器处理速度很快。但是第二次更新可能会有点延迟。用户不希望它立即完成。(实际上,我们经常使用去抖动等技术去更新。)

// 紧急
setInputValue(input);
// 不紧急
setSearchQuery(input);

在react 18之前,所有更新都被紧急渲染。这意味着上面的两个状态仍然会同时呈现,并且仍然会阻止用户看到他们交互的反馈,直到一切都呈现出来。我们缺少的是一种告诉 React 哪些更新是紧急的,哪些不是的方法。

startTransition 有什么帮助?

新的api能够将此更新进行标记。

import { startTransition } from 'react';

setInputValue(input);  // 紧急的

startTransition(() => {
// 不是很紧急的
  setSearchQuery(input);
});

包装在其中的更新startTransition被视为非紧急处理,如果出现更紧急的更新(如点击或按键),则会中断。如果用户中断转换,React 将抛出未完成的陈旧渲染工作,仅渲染最新更新。

它与 setTimeout 有何不同?

setInputValue(input);

setTimeout(() => {
  setSearchQuery(input);
}, 0);

这将延迟第二次更新,直到呈现第一次更新之后。节流和去抖动是这种技术的常见变体。

一个重要的区别是startTransition不像setTimeout那样安排在以后。它立即执行。传递给startTransition的函数同步运行,但其中的任何更新都标记为“transitions”。React将在稍后处理更新时使用此信息来决定如何呈现更新。

另一个重要的区别是setTimeout中的大屏幕更新仍然会在超时后锁定页面。如果用户在超时触发时仍在键入或与页面交互,他们仍将被阻止与页面交互。但是标记为startTransition的状态更新是可中断的,因此不会锁定页面。它们允许浏览器在渲染不同组件之间的小间隙中处理事件。如果用户输入发生更改,React将不必继续呈现用户不再感兴趣的内容。

最后,因为setTimeout只是延迟更新,所以显示加载指示器需要编写异步代码,这通常很脆弱。通过转换,React可以为您跟踪挂起状态,根据转换的当前状态进行更新,并使您能够在用户等待时显示用户加载反馈。

过渡期间的pending状态。

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

{isPending && <Loading />}

在哪里使用它?

您可以使用startTransition包装要移动到后台的任何更新。通常,这些类型的更新分为两类:

呈现速度慢:更新需要时间,React需要执行大量工作才能将UI转换为显示结果。

网络速度慢:这些更新需要时间,因为React正在等待来自网络的一些数据。这个用例与悬念紧密集成。

SSR for Suspense

SR for Suspense 解决三个主要问题:

  • 在发送 HTML 之前,您不再需要等待所有数据加载到服务器上。 相反,当您有足够的内容显示应用程序的外壳时,您立即开始发送 HTML,并在准备好时流式传输其余的 HTML。
  • 您不再需要等待所有 JavaScript 加载完毕才能开始补水。 相反,您可以将代码拆分与服务器渲染一起使用。服务器 HTML 将被保留,当相关代码加载时,React 将对其进行水合。
  • 您不再需要等待所有组件都开始与页面交互。 相反,您可以依靠 Selective Hydration 来确定用户与之交互的组件的优先级,并尽早对它们进行保湿。

在 React 18 的 server render 中,只要使用 pipeToNodeWritable 代替 renderToString 并配合 Suspense 就能解决上面三个问题。

最大的区别在于,服务端渲染由简单的 res.send 改成了 res.socket,这样渲染就从单次行为变成了持续性的行为。 react 18 SSR 效果是怎么样的呢?总结一下关键点在于‘按需’。

  1. 被 <Suspense> 包裹的区块,在服务端渲染时不会阻塞首次吞吐,而且在这个区块准备完毕后(包括异步取数)再实时打到页面中(以 HTML 模式,此时还没有 hydration),在此之前返回的是 fallback 的内容。
  2. hydration 的过程也是逐步的,这样不会导致一下执行所有完整的 js 导致页面卡顿(hydration 其实就是 React 里写的回调注册、各类 Hooks,整个应用的量非常庞大)。
  3. hydration 因为被拆成多部,React 还会提前监听鼠标点击,并提前对点击区域优先级进行 hydration,甚至能抢占已经在其他区域正在进行中的 hydration。