React v17.0 RC版本发布:无新特性(译)

1,779 阅读15分钟

2020年8月10日 作者 Dan Abramov and Rachel Nabors

今天我们发布了React 17的第一个RC版本。距离上一个主要版本发布已经过去了两年半,即使按照我们的标准这也是一个很长的时间!在此,我们将描述这次版本的职责,对你的影响,以及如何取使用它。

无新特性

React 17 版本很不寻常,因为他并没有添加任何面向开发者的功能。而专注于升级简化React本身。

我们正努力开发新的功能,但并不属于本次版本。React 17是我们深度推广的关键所在。

特别的是,React 17 是一个『垫脚石』版本,它会使由一个React版本管理的tree嵌入到另一个React管理的tree中时会更加安全。

逐步升级

过去7年,React一直遵循"all-or-nothing"的升级策略,你可以使用旧版本,也可以将整个项目升级到新版本,但没有介于两者之间的情况。

这个升级策略延续至今,但是我们遇到了"all-or-nothing"的局限性。举例来说,在做一些API的变更时,弃用旧版context API,并不能以自动化的方式来完成,即使大多数程序都没有使用过,我们仍然在React中支持它们。我们必须抉择是在React中永远支持还是把他们留在旧版本中。这两种决策都不是最佳实践。

所以,我们想提供另外一种选择。

**React17支持逐步升级React版本。**当你从React 15升到 16(或者从React 16升到 17)时,你通常会立刻升级整个项目。这种方式适用于很多项目,但如果代码库是在几年前写的,并且没有得到活跃的维护,升级可能会给你带来灾难性的挑战。尽管你可以在页面上使用两个版本的React,但React 17之前这种方式很脆弱,并会导致事件问题。

我们在React 17中修复了很多诸如上述的问题。这意味着**当React 18或未来版本发布时,你将有更多选择。**首选还是像以前一样,立即升级你的整个项目。但是你也可以将你的项目分块升级。举例来说,你可以选择将你大部分项目迁移到React 18,但是可以保留React 17中的一些延迟加载的对话框或子路由。

这并不意味着你必须逐步升级。对于大多数项目来说,一次全部升级仍然是最好的解决方案。同时使用两个版本的React - 即使其中一个按需加载 - 仍然不是很理想。然而,对于没有活跃维护的大型项目,可以考虑这种方案,并且React 17开始可以保证这些应用程序不再落伍。

为了实现逐步更新,我们需要对React事件系统做一些修改。这些修改可能会对代码产生影响,这也是React 17能成为主要版本的原因。实际上,100000+的组件中受影响的组件不超过20个,所以**我们希望大多数项目在没有特殊问题时可以升级到React 17。**如有问题请联系我们。

逐步升级Demo

我们准备了示例库来展示如必要时如何懒加载旧版本的React。Demo使用Create React App,但其他工具采用类似的方法应该都适用。我们欢迎通过其他工具来做的Demos出现。

注意

我们已经将其他更改放到React 17版本之后。这个版本的目标主要是实现逐步升级。如果升级到React 17太困难,则此目标无法实现。

事件委托的更改

技术层面来说,它始终可以嵌套不同版本的React进行开发。然而React的事件系统的工作使这种方式变得很脆弱。

在React组件中,通常会内联编写事件处理程序:

<button onClick={handleClick}>

类似于此的原始代码是:

myButton.addEventListener('click', handleClick);

然而,对于大多数事件,React实际上并没有将它们附加在生命他们的DOM节点上。取而代之的是,React会直接在document上为每中事件类型附加一个处理程序,被称为事件委托。除了在大型项目上具有性能优势外,它还使添加类似于 replaying events 这样的新特性变得更加容易。

React问世以来一直自动进行事件委托。当document上触发了DOM的事件,React会找出调用的组件,然后React事件会在组件中向上"冒泡",但实际上,原生事件已经冒泡出了document级别,React在其中注册事件处理器。

然而,这对逐步升级带来了一个问题

如果你的页面中有多个React的版本,他们都将在顶层注册事件处理器。这破坏了 e.stopPropagation(): 如果嵌套树结构中阻止了事件冒泡,其余树仍然可以接受到。这会使不同版本的React嵌套变得困难重重。这种担忧是有根据的 -- 举例来说,Atom开发人员在四年前遇到的问题

这就是为什么我们要去改变React自动将事件附加到DOM的方式。

在React 17中,React将不再在document级别附加事件处理程序。取而代之的是,它将它们附加到渲染您的React树的根DOM容器中:

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

在React 16或更早的版本,React对大多数事件做document.addEventListener() 处理。

React17与以往版本不同的是,它将会自动调用rootNode.addEventListener()

A diagram showing how React 17 attaches events to the roots rather than to the document

得益于这项改变,**现在可以更加安全地进行新旧版本React树的嵌套。**请注意,要想正常工作,所有版本必须为17或以上,这也是为什么升级到React 17是至关重要的。从某种意义上说,React 17是一个『垫脚石』版本,使逐步升级成为可能。

这项改变**还使得将React嵌入其他技术构建的项目中变得更加轻松。**举例来说,如果你使用的壳子是jQuery编写的,但项目内的新代码使用React来写的,则React代码中的e.stopPropagation()会阻止它影响jQuery的代码 - 符合预期。这在其余场景也适用。如果你不在喜欢React并想重构项目 - 比如jQuery - 你可以从外科开始捋React转换为jQuery,而不会破坏事件冒泡。

经核实,多年来issue上的许多问题都已被新特性解决。这些问题大多都与将React与非React代码集成有关。

注意

你可能很想知道这是否会破坏跟容器之外的Portals。答案是React也会监听Protal容器上的事件,所以这不是一个问题。

解决潜在问题

像每次重大更新一样,我们需要调整一些代码。在Facebook,我们再成千上万个模块中,大约调整了10个模块以适应此次更改。

举例来说,如果在模块中使用document.addEventListener(...)进行DOM监听,你可能期望它能捕获React的所有事件。在React 16或更早的版本,即使你在React事件处理器中调用e.stopPropagation() ,你创建的document监听还是会触发,因为原生事件已经在document级别。适用React 17 冒泡将被阻止(按需),所以你的document监听将不会被触发:

document.addEventListener('click', function() {
  // This custom handler will no longer receive clicks
  // from React components that called e.stopPropagation()
});

你可以将监听转换为使用capture phase来修复此类代码。如果你这样做,你可以将 { capture: true } 作为 document.addEventListener的第三个参数传递:

document.addEventListener('click', function() {
  // Now this event handler uses the capture phase,
  // so it receives *all* click events below!
}, { capture: true });

注意此策略在全局上更具灵活性 - 例如,它可能会修复代码中现有的错误,这些错误在Raect事件处理器外部调用e.stopPropagation() 发生。换句话说,React 17中的事件冒泡更接近原生DOM。

其他重大更改

我们将React 17的重大更改影响度将至最低。例如,它不会删除以前版本中弃用的任何方法。然而,它的确包含一些重大更改来使其更加安全。总之,100000+个组件中受影响的组件不会超过20个。

对标浏览器

我们对事件系统做了一些小的更改:

  • onScroll 事件不再冒泡,以防止出现常见的混淆
  • React的onFocus和onBlur事件已经在底层切换为原生的fucusin 和 focusout 事件。他们更接近React现有行为,有时还会提供额外的信息。
  • 捕获事件(例如:onClickCapture) 现在使用的是实际浏览器中的捕获监听器。

这些更改使React与浏览器行为更加接近,并提高了互通性。

注意

尽管React 17已经将onFocus事件切换到focusin,请注意,这并没有影响冒泡行为。在React中,onFocus事件永远都会冒泡,并且会在React 17中继续这样做,因为这是一个实用的默认操作。

this sandbox

移除事件池

React 17 移除了"事件池",它并不会提高现在浏览器的性能,甚至还会使有经验的React开发者困惑:

function handleChange(e) {
  setData(data => ({
    ...data,
    // This crashes in React 16 and earlier:
    text: e.target.value
  }));
}

这事因为React在旧浏览器中重用了不同事件之间的事件对象以提高性能,并将它们之间的所有事件字段设为null。在React 16或更早版本中,你必须调用e.presist()才能正确适用该事件,或读取你之前需要的属性。

在React 17中,这段代码可以如愿工作。旧的事件池优化已经完全移除,所以你可以在你需要时读取事件字段。

这改变了行为,因此我们将其标记为重大更改,但实际中我们并没有对Facebook缠身任何影响。(甚至还修复了一些错误!)。请注意,e.persist()在React中仍然可用,但是调用没有任何效果。

副作用时间清理

我们正在使useEffect 和清理函数的时间保持一致。

useEffect(() => {
  // This is the effect itself.
  return () => {    // This is its cleanup.  };});

大多数副作用不需要延迟屏幕更新,所以React把副作用放在屏幕绘制之后做异步调用。(在极少数的情况下,你需要副作用趋阻止绘制,例如,如果需要获取尺寸和位置,请使用useLayoutEffect

然而,如果存在副作用清理函数在React 16中运行,我们发现类似于componentWillUnmount在类中同步,这对大型项目并不是个好选择,因为同步会减缓屏幕的切换。(例如Tabs)。

在React 17中,副作用清理函数同样异步运行 - 例如,如果组件消失,会在屏幕绘制结束时调用清理。

这说明了副作用本身如何更紧密的运行。在极少数的情况下,你可能希望依靠同步执行,可以改用 useLayoutEffect 。

注意

你可能想知道这是否意味着你现在将无法修复有关已卸载组件上的setState的警告。无需担心,React特别检查了这种情况,不会在卸载和清理之前的短暂间隔内触发警告。因此取消代码请求或者间隔基本可以保持不变。

另外,React 17会根据他们在tree中的未知,以与效果相同的顺序执行清除功能。在此版本之前顺序有时会不同。

隐患

可复用的库可能需要对此情况进行深度测试,但我们只遇到了几个组件会因为此问题中断执行。其中一个有问题的代码的示例:

useEffect(() => {
  someRef.current.someSetupMethod();
  return () => {
    someRef.current.someCleanupMethod();
  };
});

问题在于 someRef.current 是可变的,因此在运行清理函数时,它很有可能被置为null。解决方案是在副作用内部存储会发生变化的值:

useEffect(() => {
  const instance = someRef.current;
  instance.someSetupMethod();
  return () => {
    instance.someCleanupMethod();
  };
});

我们不希望此问题对大家造成影响,我们提供了 [eslint-plugin-react-hooks/exhaustive-deps](https://github.com/facebook/react/tree/master/packages/eslint-plugin-react-hooks)检测规则(请确保在项目中使用它)会对此情况发出警告。

返回一致的undefined错误

在React 16或早期版本中,返回undefined始终是一个错误:

function Button() {
  return; // Error: Nothing was returned from render
}

部分原因是很容易无意间返回undefined:

function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

以前,React只会对class和函数组件执行此操作,但并不会检查forwardRef和memo组件返回的值。这是由于编码错误导致。

在React 17中,forwordRef 和 memo 组件的行为会与常规函数组件和class组件保持一致。在返回undefined时会报错。

let Button = forwardRef(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

let Button = memo(() => {
  // We forgot to write return, so this component returns undefined.
  // React 17 surfaces this as an error instead of ignoring it.
  <button />;
});

对于你要故意不渲染任何内容的情况,请返回null。

原生组件栈

当你在浏览器中遇到错误时,浏览器会为你提供带有JavaScript函数的名称及位置的堆栈信息。然而JavaScript堆栈通常不足以诊断问题,因为React树的层次结构可能同样重要。你不仅要知道那个Button抛出了错误,而且还想要指导Button在React树中的哪个位置。

为了解决这个问题,当你遇到错误时,从 React 16 开始会打印 "组件栈" 信息。尽管如此,它们仍然不如原生的 JavaScript 堆栈。特别是,它们在控制台中不可点击,因为 React 不知道函数在源代码中的声明位置。此外,它们在生产中几乎无用。不同于常规压缩后的 JavaScript 堆栈,它们可以通过 sourcemap 的形式自动恢复到原始函数的位置,而使用 React 组件栈,在生产环境下必须在堆栈信息和 bundle 大小间进行选择。

在 React 17 中,使用了不同的机制生成组件堆栈,该机制会将它们与常规的原生 JavaScript 堆栈缝合在一起。这使得你可以在生产环境中获得完全符号化的 React 组件堆栈信息。

React 实现这一点的方式有点非常规。目前,浏览器无法提供获取函数堆栈框架(源文件和位置)的方法。因此,当 React 捕获到错误时,将通过组件上述组件内部抛出的临时错误(并捕获)来重建其组件堆栈信息。这会增加崩溃时的性能损失,但每个组件类型只会发生一次。

如果你对此感兴趣,可以在这个 PR 中阅读更多详细信息,但是在大多数情况下,这种机制不会影响你的代码。从使用者的角度来看,新功能就是可以单击组件堆栈(因为它们依赖于本机浏览器堆栈框架),并且可以像常规 JavaScript 错误那样在生产中进行解码。

构成重大变化的部分是,要使此功能正常工作,React 将在捕获错误后在堆栈中重新执行上面某些函数和某些 class 构造函数的部分。由于渲染函数和 class 构造函数不应具有副作用(这对于 SSR 也很重要),因此这不会造成任何实际问题。

移除私有导出

最后,值得注意的重大变化是我们删除了一些以前暴露给其他项目的 React 内部组件。特别是,React Native for Web 过去常常依赖于事件系统的某些内部组件,但这种依赖关系很脆弱且经常被破坏。

在 React 17 中,这些私有导出已被移除。据我们所知,React Native for Web 是唯一使用它们的项目,它们已经完成了向不依赖那些私有导出函数的其他方法迁移。

这意味着旧版本的 React Native for Web 不会与 React 17 兼容,但是新版本可以使用它。实际上,并没有太大的变化,因为 React Native for Web 必须发布新版本以适应其内部 React 的变化。

另外,我们删除了 ReactTestUtils.SimulateNative 的 helper 方法。他们从未被记录,没有按照他们名字所暗示的那样去做,也没有处理我们对事件系统所做的更改。如果你想要一种简便的方式来触发测试中原生浏览器的事件,请改用 React Testing Library。

安装

我们鼓励你尽快尝试 React 17.0 RC 版本,在迁移过程中遇到任何问题都可以向我们提出。 请注意,候选版本没有稳定版本稳定,因此请不要将其部署到生产环境。

通过 npm 安装 React 17 RC 版,请执行:

npm install react@17.0.0-rc.0 react-dom@17.0.0-rc.0

通过 yarn 安装 React 17 RC 版,请执行:

yarn add react@17.0.0-rc.0 react-dom@17.0.0-rc.0

我们还通过 CDN 提供了 React RC 的 UMD 构建版本:

<script crossorigin src="https://unpkg.com/react@17.0.0-rc.0/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17.0.0-rc.0/umd/react-dom.production.min.js"></script>

有关详细安装说明,请参阅文档

最后

翻译不易,走过路过请给作者留个小小的赞,感谢一路同行的你们!