[译]React17官方提前说明

2,704 阅读15分钟

翻译完后才发现NickJiangDev已经翻译完了,但是我们两个在某些地方翻译有点不同,就也放上来了,有些地方有参考NickJiangDev同学译文进行优化,十分感谢,NickJiangDev的译文

React17

无新特性

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太困难,则此目标无法实现。

改变Event Delegation

从技术上讲,总是可以嵌套使用不同版本的Reaction开发的应用程序。 然而,由于React事件系统的工作方式,它相当脆弱。

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

<button onClick={handleClick}>

类似于此的原始代码是:

myButton.addEventListener('click', handleClick);

但是,对于大多数事件,React实际上不会将它们附加到在其上声明它们的DOM节点上。 相反,React会直接在document node 上为每种事件类型附加一个处理程序。 . 这称为 event delegation.除了在大型应用程序树上具有性能优势外,它还使添加新功能例如replaying events这样的新特性变得更加容易.

自从其发布以来,React一直自动进行事件委托。 当document上触发DOM事件时,React会找出要调用的组件,然后React事件会在整个组件中“冒泡”。但实际上,原生事件已经冒到docment级别,React在其中注册事件处理器。

然而,这对于逐步升级来说是个问题。

如果页面上有多个React版本,则它们都会在顶部注册事件处理程序。 这会中断e.stopPropagation(): 如果嵌套树已停止事件冒泡,其他外部树仍会接收该事件。这使得难以嵌套不同版本的React.这种担忧不是假设的-例如, 4年前的的 Atom editor ran into this.

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

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

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

在React16和更之前的版本使用document.addEventListener() 对于大多数事件. React 17之后说取而代之的是rootNode.addEventListener()` .

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

多亏了这一更改,现在将一个版本管理的Reac树嵌入到另一个React版本管理的树中更加安全. 请注意,要实现这一点,两个版本都需要17或更高版本,这就是为什么升级到React17非常重要。 在某种程度上,React17是一个“stepping stone”版本,它使得接下来的逐步升级成为可能.

这项更改还使得将React嵌入使用其他技术构建的应用程序中变得更加容易.例如,如果您的应用程序的外层“shell”是用jQuery编写的,但其中较新的代码是用React编写的,那么React代码中的e.stopPropagation()现在会阻止它访问jQuery代码---正如您所预期的那样: 这反过来也是有效的。 如果您不再喜欢React,并且想要重构您的应用程序-例如,使用jQuery中-您可以开始将外层shell从React转换为jQuery,而不会中断事件传播。

我们已经确认,多年来,我们在问题跟踪器上报告的许多问题(原文每一个单词都是一个高楼的git issues地址, 怨念太深了)已通过新行为解决了,这些问题与将React与非React代码集成有关。

Note

您可能想知道这是否会破坏根容器之外的Portal。 答案是,React也侦听 portal containers上的事件,因此这不是问题。

Fixing Potential Issues

与任何重大更改一样,很可能需要调整一些代码。 在Facebook,我们必须总共调整大约10个模块(成千上万个模块中)以适应此更改。.

例如,如果使用 DOM listeners with document.addEventListener(...), 您可能期望他们catch所有React事件.在React 16及更早版本中, 即使使用l e.stopPropagation() 在 React event handler, 你的document listeners 任然会接受到它们,因为这些原生事件已经 already 在document level. 在React 17中,这种操作 stop (as requested!), 所以你的 document handlers 不会工作:

document.addEventListener('click', function() {
  //该自定义处理程序将不再接收click。
	//来自调用e.stopPropagation()的React组件
});

你可以修复代码, 像将你的listener使用 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 });

注意这个策略总体上是如何更具弹性的 — 例如:它可能会修复代码中在React event handler外部调用e.stopPropagation() 时发生的现有错误.换句话说, React17中的事件传播更接近于常规DOM.


其他重大变化

我们将React 17中的重大更改保持在最低水平。 例如,它不会删除以前版本中已弃用的任何方法。 但是,它的确包含一些其他重大更改,根据我们的经验,这些更改相对安全。 总计,由于这些因素,我们必须在100,000多个组件中需要调整的组件少于20个。

与游览器保持一致

我们对事件系统做了几个较小的更改:

  • onScroll event 不在冒泡 防止常见的混乱.
  • React onFocusonBlur events 已经切换为在底层使用原生的 focusinfocusout events ,这与React的现有行为更加接近,有时还会提供额外的信息.
  • 捕获阶段events (e.g. onClickCapture) 现在使用真实的浏览器捕获listeners.

focusin: 触发是focus不触发的时候, focusin会冒泡,而focus不会

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

Note

尽管React 17在 onFocus event中源码从 focus 转变为使用 focusin, 请注意,这并没有影响冒泡行为. 在React中, onFocus event 总是冒泡的. 并且它在React17中继续这样做,因为通常它是更有用的缺省. 看this sandbox 您可以针对不同的特定用例添加不同的检查

移除事件池

React17从React中移除了"事件池"优化.它不会提高现代浏览器的性能,甚至会使经验丰富的React用户感到困惑:

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

这是因为React重用了不同事件之间的事件对象以提高旧浏览器的性能,并将它们之间的所有事件字段设置为空。 对于React16和更早版本,您必须调用e.Persistent()才能正确使用事件,或者读取前面需要的属性。

在React 17中,此代码可以按您期望的那样工作。 旧的事件池优化已被完全删除,因此您可以在需要时读取event字段

这是一种行为更改,这就是我们将其标记为破坏的原因,但实际上,在Facebook上我们还没有看到它破坏任何东西。 (也许它甚至修复了一些错误!)请注意,e.persist()在React事件对象上仍然可用,但是现在它什么也没做。

Effect清理定时

我们使useEffect cleanup 功能的时间安排更加一致。

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

大多数effects不需要延迟屏幕更新,因此React在在屏幕上reflected更新之后就异步运行它们。 (对于需要effects来阻止绘制的极少数情况,例如测量和定位工具提示,请首选useLayoutEffect)

然而,Effect Cleanup功能(如果存在)通常会在React 16中同步运行。我们发现,类似于ComponentWillUnmount在类中同步运行,这对于较大的应用程序并不理想,因为它会减慢大屏幕转换(例如切换选项卡)的速度。

在React 17中,effect cleanup function也会异步运行-例如,如果组件正在卸载,则清理将在屏幕更新后运行

这反映了effects本身是如何更紧密地运行的。 在可能需要依赖同步执行的极少数情况下,您可以改为useLayoutEffect

Note

您可能想知道这是否意味着您现在无法修复有关卸载组件上的setState的警告。 不要担心-React专门检查这种情况,并且不会在卸载和清理之间的较短时间内发出setState警告。 因此,取消请求或定时器的代码几乎可以始终保持不变

另外,React 17根据它们在树中的位置,以与effects定义相同的顺序执行清除功能。 以前,此顺序有时会有所不同。

潜在问题

虽然可复用的库可能需要对其进行更彻底的测试,但我们只看到几个组件与此更改发生了冲突。 有问题的代码的一个示例可能如下所示:

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

问题在于 someRef.current是可变的,所以在执行清理程序时,其有可能被设置为 null. 解决方案是捕获effect 的任何可变值:

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

我们不希望此问题对大家造成影响,我们提供了our eslint-plugin-react-hooks/exhaustive-deps lint rule检测规则(请确保在项目中使用它)会对此情况发出警告。

Returning 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仅对类和函数组件执行此操作,但不检查forwardRefmemo组件的返回值。 这是由于编码错误.

在React 17中,forwardRefmemo组件的行为与常规函数和类组件一致。 它们返回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.

原生组件堆栈

在游览器中你throw error, 浏览器为您提供带有JavaScript函数名称及其位置的堆栈跟踪. 然而, JavaScript stacks 通常不足以诊断问题因为React树的层次结构可能同样重要. 您不仅要知道Button 抛出错误, 但是这个Button 是在React tree中的哪 .

为了解决这个问题,当您遇到错误时,React 16开始打印“组件堆栈”. 尽管如此,它们仍然不如原生JavaScript stacks.它们在控制台中不可点击, 因为React不知道函数在源代码中的声明位置. 此外, 这些在生产中几乎毫无用处. 与常规的精简JavaScript stacks不同,后者可以通过Sourcemap自动恢复为原始函数名称, 使用React component stacks你必须在生产堆栈和捆绑包大小之间进行选择.

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

React实现这一点的方式有些不合常规。 当前,浏览器无法提供获取function’s stack fram(源文件和位置)的方法。 因此,当React捕获到错误时,现在,它在可能的情况下将通过从抛出错误的组件上面的每个组件内部抛出(并捕获)一个临时错误来重建其组件堆栈。 这会增加少量的崩溃性能损失,但每个组件类型只会发生一次.

如果您感兴趣,可以在 this pull request阅读更多详细信息, 但在大多数情况下,这种确切的机制应该不会影响您的代码.从您的角度来看,新功能是现在可以单击报错信息中组件堆栈(因为它们依赖于本机浏览器堆栈框架),并且您可以像处理常规JavaScript错误一样在生产中对它们进行解码.

构成重大变化的部分是为了使之起作用,捕获错误后,React在堆栈中重新执行上述某些React函数和React类构造函数的部分.由于render函数和类构造函数不应该有副作用(这对于SSR也很重要),这应该不会带来任何实际问题.

删除private Exports

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

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

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

此外,我们还删除了 ReactTestUtils.SimulateNative 辅助方法.它们从未被记录在案,没有完全按照它们的名字来做,也没有使用我们对事件系统所做的更改.如果您想要一种方便的方法来在测试中触发本机浏览器事件,请查看React Testing Library.