React 18 ——— 官方 blog 来了!

1,251 阅读5分钟

寒冬已逝,万物复苏,React 18 也在这个百花盛开的时刻来临了。在新的版本中,React 官方团队为我们带来了一些新的特性,并且在这个版本中,之前的一些常见接口和方法也都不再适用,这对于想要升级 React 18 的团队来说确实是一个挑战。那么接下来我们就介绍一下 React 18版本中特性的改变。

ReactDOM.render

React 18 最大的一个改动就是不再支持 ReactDOM.render,而是使用 createRoot 来做替代,之所以要做这样的改变官方的理由有两个:

  1. 可以更好地人为控制 root 节点
  2. 可以有选择地控制并发特性

我们来看一个例子:

// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);

从上面这个例子我们可以看出来,在 React 18 以前,我们创建根节点和将 React 组件渲染到根节点必须同时发生。然而当前版本中,render 函数被挂载到了根节点上,我们就可以先创建根节点,并且控制 React 组件的渲染时机。

改变了 render 函数带来的第二点是 unmountComponentAtNode, 在 render 函数挂载到 root 节点之后,unmount 函数也挂载到了 root 节点。从前我们如果想要从根元素上卸载组件,我们需要这样做:

unmountComponentAtNode(container);

现在就直接在 root 节点上调用 unmount 函数就好:

root.unmount();

同时 render 函数的第三个参数 callback 也被移除掉了,因为它和 Suspense同时使用时经常会带来不一样的结果。如果还是想要实现这个功能呢,我们就应该像下面样:

// Before
const container = document.getElementById('app');
ReactDOM.render(<App tab="home" />, container, () => {
  console.log('rendered');
});

// After
function AppWithCallbackAfterRender() {
  useEffect(() => {
    console.log('rendered');
  });

return <App tab="home" />
}

const container = document.getElementById('app');
const root = ReactDOM.createRoot(container);
root.render(<AppWithCallbackAfterRender />);

最后呢,如果说你的页面是服务端渲染的,就需要替代 hydrate

// Before
import { hydrate } from 'react-dom';
const container = document.getElementById('app');
hydrate(<App tab="home" />, container);

// After
import { hydrateRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here.

注意:是不是我们如果升级到 18,上面的接口都会报错呢?不是的,而是上面的新特性都会无效,依旧按照 17 的方式来表现

自动 "批量" 处理

React 18 通过增加在初始状态下的 批量处理 来提升额外的性能。所谓的 批量处理 就是指将多组的 state更新在一次渲染内完成来获取更好的性能。在 React 18 之前,我们只会在 React 事件处理中进行批量更新。但是比如说在 promisessetTimeout或者原生的事件绑定中,我们都不会在初始状态下批量处理state

// Before React 18 only React events were batched

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will render twice, once for each state update (no batching)
}, 1000);

在上面的代码中,handleClick 这个函数中会一次性将所有的 state更新完之后进行渲染。但是在 setTimeout 中,每次 state 的更新都会被渲染。

从 React 18 使用 createRoot 开始,所有的更新都会自动地被批量处理,无论它们是写在哪里的,也就是说下面这段代码:

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will render twice, once for each state update (no batching)
}, 1000);

在这段代码中,虽然有两个 state 更新,但是其实 React 只渲染一次。

这在 React 中可以说是一个很大的突破,其目的是为了减少 render 的工作量并且能在你的 App 中提升更多的性能。如果你不想使用 自动批量操作,你可以使用 flushSync 接口:

import { flushSync } from 'react-dom';

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
}

更新严格模式

在未来,React 想要在保存 state 的同时可以添加和移除 UI 块。例如,一个面板上,用户使用 tab可以快速地从 A 面板切换到 B 面板,而我希望在切换到 B 面板的时候,可以保留 A 面板的 state,这样在切回来的时候,我就可以即时地去渲染之前的那个页面。为了做到这一点,React 在 unmountmount 时使用的是相同的组件 state

这样的特性可以提高 React 的性能,但是这同样要求组件对于频繁的 mountunmount 产生的effects 有能够灵活处理的能力。大多数的 effects 都不会造成任何改变和影响,但是有一些 effects 会认为组件只会被 mount 和销毁一次。

为了解决这些问题,React 18 对于 Strict Mode 介绍了一种 development-only 这种检查机制。这种检查会自动 mount 或者 unmount 每一个组件,无论是组件第一次被 mount,还是组件已经被 mount 过一次,保存了之前的 state,从而第二次被 mount

在这次改变之前,React 会 mount 组件并创建下面这种 effects

* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.

在 React 18 的 Strict Mode 中,React 会在 development mode 中模拟 unmountremount

* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.
* React simulates unmounting the component.
    * Layout effects are destroyed.
    * Effects are destroyed.
* React simulates mounting the component with the previous state.
    * Layout effect setup code runs
    * Effect setup code runs

配置你的测试环境

当你使用 createRoot 去更新你的单元测试环境的时候,你可能会收到如下的 warning:

// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

这个标志位的意义就是去告诉 React 当前正在一个单元测试的环境中运行,如果你忘记在你的更新外层包裹 act, 那么 React 就会直接 console 出来对应的 warning

你也可以将这个标志位设置成为 false 来告诉 React 这个最外层包裹着的 act 是不需要的,这尤其适合那些完全模拟浏览器环境的端到端测试。

放弃支持 IE

在这个 release 版本中,React 正在逐渐放弃 IE 浏览器。这是因为在 React 18 中,很多的新特性是建立在现代浏览器的基础上的,比如说像 microtasks 就没有办法完全在 IE 上进行 polyfill,因此如果你的应用要兼容 IE, 就不要升级。