React18

360 阅读7分钟

Vue3已经成为Vue的正式版本了,相信React18也离我们不远了。本文主要介绍一下React18新增了哪些的特性,以及对现有特性有哪些开箱即用的改进。

Automatic batching

要理解这个概念我们首先要理解什么是batching,下面我们就来了解下batching这个概念。

Batching

batching就是将多次state更新合并到一次re-render中,来减少渲染次数从而达到性能优化的目的。batching机制不仅避免了不必要的渲染,同时也避免了组件渲染到一半就又要执行下一次渲染的问题,这个问题在实际开发中很容易造成bug。

我们都知道目前为止React的batching是没有办法覆盖所有场景的,比如用户在promise/setTimeout/原生dom事件/其他自定义事件中更新状态是无法合并到一次re-render中的;总的来说就是在不受React控制地方更新状态是无法合并的。比如你要在调用某个接口之后再更新状态,请看下面代码:

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

  function handleClick() {
    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={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

但在React18中就不会有问题,下面我们在来了解下什么是automatic batching

what is Automatic Batching

从React18createRoot之后,无论用户在什么地方更新状态都即将会是batching的。这意味着在promise/setTimeout/原生dom事件/其他自定义事件中更新多次状态不会引起多次渲染,请看下面代码:

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

  function handleClick() {
    fetchSomething().then(() => {
      // React 18 and later DOES batch these:
      setCount(c => c + 1);
      setFlag(f => !f);
      // React will only re-render once at the end (that's batching!)
    });
  }

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

What if I don’t want to batch?

automatic batching通常是十分有用的,但是在极个别的情况下也许我们还是需要在每次更新状态后立即重新渲染,那么你可以利用ReactDOM.flushSync()来实现强制re-render,请看下面代码:

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
}

startTransition

要搞清什么是startTransition,就要搞清楚前端渲染任务从紧急程度来说可以分为urgent(紧急的)和transition(过渡的,即在一定时间内延迟渲染是可以被接受的)。

举个例子,用户在输入框内输入内容就属于urgent(紧急渲染任务),输入延迟对于用户来说是不可接受的。但是输入提示就是一个非紧急渲染任务,那么这种情况我们就可以利用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);
});

与setTimeout的区别

这么看startTransitionsetTimeout似乎有点类似,那为啥不使用setTimeout而要另外开发一个startTransition呢?这就要去了解startTransition做了些什么。

首先说一下setTimeout的机制,如下代码,最优的情况下setTimeout的回调会被延迟200ms被执行,然后更新状态后进行re-render。这也就意味着使用setTimeout一定会有所延迟,而startTransition并不是这样的。

setTimeout(() => {
    setCounter(c => c + 1);
}, 200)

startTransition中的回调会被立即执行(同步执行),在其中执行更新状态,react会立即执行状态的更新,并将更新状态的计算结果缓存住,然后寻找合适的时机(渲染空闲)再执行re-render

react是如何知道浏览器的渲染空闲的呢?

这得益于reactfiber架构,在React16中实现了fiber,其原理是利用了requestIdleCallbackrequestAnimationFrame两个api。至于fiber requestIdleCallback requestAnimationFrame具体细节这里就不在细究,大家可以自行google

useTransition

通过useTransition我们还能拿到一个isPending参数来表示这个渲染更新是否正在执行,请看下面代码:

import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

通过isPending我们可以给视图加一个pending的过渡状态,从而优化用户体验。

New Suspense SSR Architecture

要了解这个我们就要先将SSRSuspense这两个概念搞清楚。

什么是SSR

SSRServer Side Render(服务端渲染)。服务端渲染主要是为了让我们在网络不太好的情况下看到这个画面:

image.png

而不是这个画面:

image.png

什么意思呢?就是客户端渲染仅当javascript代码(包含react、其他框架/库、及业务代码)下载完成后才能看到页面并进行操作。在网络不好的状态下,下载js会是个相对漫长的过程,在下载完成之后客户看到的画面可能就是一篇空白(单页应用js未下载完成,loading都是不会展示的)。这是个极其不好的体验,而SSR可以很好的解决这个问题。SSR在网络较慢的情况下即使javascript代码没有下载完成也能浏览页面大致的样子(但不能进行交互操作)。

但是SSR也并非完美无缺,比如SSR首次加载必须将当前页面所有内容都渲染出来,然后再进行下载js等后续操作。而在渲染好后到js下载完成这段时间用户是不能进行任何别的交互操作。那么有些并非必须第一时间渲染的内容就会造成阻塞。这就需要我们下一位主角登场了:Suspense

什么是Suspense

Suspense并非是新的概念。Suspense其实在React的客户端渲染中是很常见的。他配合React.lazy可以组件异步化(对这个还不太了解的小伙伴就请自行google吧)。

而从React18开始,服务端渲染也可以使用Suspense了。请看下面代码:

<Layout>
    <NavBar />
    <SideBar />
    <RightPanel>
        <Post />
	<Suspense fallback={<Spining />}>
            <Comments />
	</Suspense>
    </RightPanel>
</Layout>

这么写在第一次SSR时并不会直接渲染<Comments />组件,取而代之的是<Spining />组件。只有当水合(hydrate)完成之后才会被渲染。

什么是水合(hydrate)?

所谓水合,就是完成SSR渲染、下载了js文件,执行react引擎将所有需要展示的数据计算好再重新渲染完成。

代码层面需要做哪些改动?

说了那么多,其实开发者最关心的是在以往的代码上要做出哪些改动。

以往我们在做服务端渲染的时候都会用一个api renderToString

renderToString(React.Node): string

  • 在react18之前,在 renderToString 中传入<Suspense>包裹的组件将会抛出异常。
  • react18之后,在 renderToString 中传入<Suspense>包裹的组件,react会退出服务端渲染

我们要在服务端渲染中真正用<Suspense>的话只需要将renderToString替换成renderToPipeableStream就行了。当然,还有个renderToNodeStream,但这个即将被弃用这里就不做讨论了。

renderToPipeableStream(React.Node, Options): Controls

Concurrent

React18又引入了概念Concurrent,即并发渲染。那么ConcurrentParallel(并行)的区别是什么呢?

  • 并行是指真正意义上的同时运行,即多个cpu处理多个任务。
  • 并发是指交替运行从而达到看起来同时运行的效果,并非真正的同时运行。比如startTransition就是并发,它本质是等待合适的时间去执行,而不是与其他任务同时执行。

总结

React18是非常值得期待的。虽然它带来了巨大的更新,但是大部分对于开发者来说都时开箱即用的,所谓开箱即用就是不需要在原有代码上做出很大的改动就能享有这些新特性,非常nice。期待它早日来临吧!