本文翻译自React官网: React v17.0 Release Candidate: No New Features
今天,我们正在发布React 17的第一个候选发布版。距离上一个主要的React版本已经过去了两年半,即使按照我们的标准,也已经很长时间了!在此博客文章中,我们将描述此主要版本的作用,可以期望的主要变化以及如何试用该版本。
没有新功能
React 17版本不寻常,因为它没有添加任何面向开发人员的新功能。取而代之的是,此发行版主要侧重于使其更易于升级React本身。
我们正在积极开发新的React功能,但它们不是此版本的一部分。React 17发行版是我们将其推广到任何人的战略的关键部分。
特别地,React 17是一个“垫脚石”版本,它使将由一个版本的React管理的树嵌入到由另一个版本的React管理的树中更加安全。
逐步升级
在过去的七年中,React升级一直是“全有或全无”。您可以使用旧版本,也可以将整个应用升级到新版本。中间没有。
到目前为止,这已经解决了,但是我们遇到了“全有或全无”升级策略的局限性。某些API更改(例如,不赞成使用旧版上下文API)是不可能以自动化方式进行的。即使今天编写的大多数应用程序从未使用过它们,我们仍然在React中支持它们。我们必须选择无限期地在React中支持它们,还是将某些应用程序留在旧版本的React中。这两个选项都不是很好。
因此,我们想提供另一种选择。
React 17支持逐步的React升级。从React 15升级到16(或者很快从React 16升级到17)时,通常会立即升级整个应用程序。这适用于许多应用程序。但是,如果代码库是在几年前编写的,并且没有得到积极维护,则挑战可能会越来越大。尽管可以在页面上使用两个版本的React,但是直到React 17仍然脆弱,并导致事件问题。
我们正在使用React 17解决许多这些问题。这意味着当React 18和下一个未来版本问世时,您现在将有更多选择。第一种选择是像以前一样,一次升级整个应用程序。但是您也可以选择逐个升级您的应用程序。例如,您可能决定将大部分应用程序迁移到React 18,但在React 17上保留一些延迟加载的对话框或子路由。
这并不意味着您必须逐步升级。对于大多数应用程序,一次全部升级仍然是最好的解决方案。加载两个版本的React(即使其中一个是按需延迟加载)仍然不是理想的选择。但是,对于没有积极维护的大型应用程序,可以考虑使用此选项,并且React 17可以使这些应用程序不落伍。
要启用渐进式更新,我们需要对React事件系统进行一些更改。React 17是主要版本,因为这些更改可能会被破坏。实际上,我们只需要在100,000个以上的组件中更改少于二十个组件,因此我们希望大多数应用程序可以升级到React 17而不会带来太多麻烦。
逐步升级演示
我们准备了一个例子展示了如何在必要时延迟加载旧版本的React。该演示使用Create React App,但应该可以对其他工具采用类似的方法。我们欢迎使用其他工具作为拉取请求的演示。
我们已将其他更改推迟到React 17之后。此版本的目标是实现逐步升级。如果升级到React 17太困难了,那将无法实现其目标。
对事件委派的更改
从技术上讲,始终可以嵌套使用不同版本的React开发的应用程序。但是,由于React事件系统的工作原理,它相当脆弱。
在React组件中,通常会内联编写事件处理程序:
等效于此代码的原始DOM类似于:
myButton.addEventListener('click', handleClick);
但是,对于大多数事件,React实际上不会将它们附加到在其上声明它们的DOM节点上。相反,React会直接在document
节点上为每种事件类型附加一个处理程序。这称为事件委托。除了在大型应用程序树上具有性能优势外,它还使添加新功能(如重播事件)更加容易。
自从第一个版本发布以来,React一直在自动进行事件委派。当文档上触发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( , rootNode);
在React 16和更早的版本中,React会document.addEventListener()
处理大多数事件。React 17将rootNode.addEventListener()
在后台调用。
由于此更改,现在可以更安全地将由一个版本管理的React树嵌入到由其他React版本管理的树中。请注意,要使其正常工作,两个版本都必须为17或更高版本,这就是为什么升级到React 17很重要的原因。从某种意义上说,React 17是一个“垫脚石”版本,使下一个逐步升级成为可能。
这一变化还使将React嵌入到使用其他技术构建的应用程序中变得更加容易。例如,如果应用程序的外部“外壳”是用jQuery编写的,但其中的较新代码是用React编写的,e.stopPropagation()
那么React代码内部现在将阻止它到达jQuery代码-正如您所期望的那样。这在另一个方向上也起作用。如果您不再喜欢React并想重写您的应用程序(例如,在jQuery中),则可以开始将外壳从React转换为jQuery,而不会破坏事件传播。
我们已经证实,很多 问题 报道 过 的 年 对 我们的 问题 跟踪器与整合与之反应的非反应的代码已经被固定在新的行为。
您可能想知道这是否会破坏根容器之外的Portal。答案是,React也侦听门户网站容器上的事件,因此这不是问题。
解决潜在问题
与任何重大更改一样,可能需要调整一些代码。在Facebook,我们必须调整总共约10个模块(成千上万个模块)以适应此更改。
例如,如果您通过添加手动DOM侦听器document.addEventListener(...)
,则可能希望它们捕获所有React事件。在React 16及更早版本中,即使您调用e.stopPropagation()
React事件处理程序,您的自定义document
侦听器仍会收到它们,因为本机事件已经在文档级别。使用React 17,传播将停止(按要求!),因此您的document
处理程序将不会触发:
document.addEventListener('click', function() {
// This custom handler will no longer receive clicks
// from React components that called e.stopPropagation()
});
您可以通过将侦听器转换为使用捕获阶段来修复此类代码。为此,您可以将{ 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 });
请注意,此策略在整体上如何更具弹性-例如,它可能会修复代码中在e.stopPropagation()
React事件处理程序外部调用时发生的现有错误。换句话说,React 17中的事件传播更接近常规DOM。
其他重大变化
我们将React 17中的重大更改保持在最低水平。例如,它不会删除以前版本中已弃用的任何方法。但是,它的确包含一些其他重大更改,根据我们的经验,这些更改相对安全。总体而言,由于这些因素,我们必须在100,000+个组件中调整少于20个。
与浏览器对应
我们对事件系统进行了一些较小的更改:
- 该
onScroll
事件不再冒泡, 以防止常见的混乱。 - React
onFocus
和onBlur
event已转为使用幕后的nativefocusin
和focusout
events,这与React的现有行为更加接近,有时还会提供额外的信息。 - 捕获阶段事件(例如
onClickCapture
)现在使用真实的浏览器捕获阶段侦听器。
这些更改使React与浏览器行为更加接近,并提高了互操作性。
尽管该事件从React 17切换
focus
到focusin
了幕后,但onFocus
请注意,这并未影响冒泡行为。在React中,onFocus
事件总是冒泡的,它在React 17中继续冒泡,因为通常它是一个更有用的默认值。请参阅此沙箱,了解可以针对不同的特定用例添加的不同检查。
没有事件池
React 17从React移除了“事件池”优化。它不会提高现代浏览器的性能,甚至会使经验丰富的React用户感到困惑:
function handleChange(e) {
setData(data => ({
...data,
// This crashes in React 16 and earlier:
text: e.target.value
}));
}
这是因为React在旧浏览器中重用了不同事件之间的事件对象以提高性能,并将所有事件字段都设置null
在它们之间。在React 16及更早版本中,您必须调用e.persist()
以正确使用该事件,或读取您之前需要的属性。
在React 17中,此代码可以按您期望的那样工作。旧的事件池优化已被完全删除,因此您可以在需要时阅读事件字段。
这是一种行为更改,这就是我们将其标记为破坏的原因,但实际上,在Facebook上我们还没有看到它破坏任何东西。(也许它甚至修复了一些错误!)请注意,e.persist()
React事件对象仍然可用,但是现在它什么也没做。
高效的清理
我们正在使useEffect
清理功能的时间更加一致。
useEffect(() => {
// This is the effect itself.
return () => { // This is its cleanup. };});
大多数效果不需要延迟屏幕更新,因此React在屏幕上反映出更新后立即异步运行它们。(在极少数情况下,您需要一种效果来阻止油漆,例如,测量和定位工具提示,请使用useLayoutEffect
。)
但是,在卸载组件时,效果清理函数将用于同步运行(类似于componentWillUnmount
类中的同步)。我们发现这不适用于大型应用程序,因为它会减慢大屏幕过渡(例如切换选项卡)的速度。
在React 17中,清除功能始终异步运行-例如,如果要卸载组件,则在更新屏幕后运行清除。**
这反映了效果本身如何更紧密地运行。在极少数情况下,您可能希望依靠同步执行,可以useLayoutEffect
改为使用。
您可能想知道这是否意味着您现在将无法修复有关
setState
未安装组件的警告。别担心-专门针对这种情况作出反应检查,确实没有触发setState
在卸载和清理之间的短间隔警告。因此,取消代码的请求或间隔几乎总是可以保持不变。
另外,React 17将在运行任何新效果之前始终执行所有效果清理功能(针对所有组件)。React 16仅保证组件中效果的这种顺序。
潜在问题
尽管可重用的库可能需要对其进行更彻底的测试,但我们仅看到几个组件随此更改而中断。有问题的代码的一个示例可能如下所示:
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
(请确保您使用它!)始终对此发出警告。
返回未定义的一致错误
在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.
;
}
以前,React仅对类和函数组件执行此操作,但不检查forwardRef
andmemo
组件的返回值。这是由于编码错误。
在React 17中,forwardRef
和memo
组件的行为与常规函数和类组件一致。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.
;
});
let Button = memo(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
;
});
对于您要有意不渲染任何内容的情况,请改为返回null
。
本机组件堆栈
当您在浏览器中引发错误时,浏览器会为您提供带有JavaScript函数名称及其位置的堆栈跟踪。但是,JavaScript堆栈通常不足以诊断问题,因为React树的层次结构可能同样重要。您不仅要知道Button
引发了错误,还想知道在React树中的哪个位置Button
。
为了解决这个问题,当您遇到错误时,React 16开始打印“组件堆栈”。尽管如此,它们仍然不如原生JavaScript堆栈。特别是,它们在控制台中不可单击,因为React不知道函数在源代码中声明的位置。此外,它们在生产中几乎毫无用处。与常规的最小化JavaScript堆栈可以通过源映射自动恢复到原始函数名称不同,使用React组件堆栈,您必须在生产堆栈和捆绑包大小之间进行选择。
在React 17中,使用不同的机制生成组件堆栈,该机制将它们与常规的本机JavaScript堆栈缝合在一起。这使您可以在生产环境中获得完全符号化的React组件堆栈跟踪。
React实现这一点的方式有些不合常规。当前,浏览器没有提供获取函数的堆栈框架(源文件和位置)的方法。因此,当React捕获到错误时,它现在将通过在可能的情况下从上面每个组件内部抛出(并捕获)一个临时错误来重建其组件堆栈。这会增加少量的崩溃性能损失,但是每个组件类型只会发生一次。
如果您感到好奇,可以在pull请求中阅读更多详细信息,但是在大多数情况下,这种确切的机制不会影响您的代码。从您的角度来看,新功能是现在可以单击组件堆栈(因为它们依赖于本机浏览器堆栈框架),并且可以像常规JavaScript错误那样在生产中对其进行解码。
构成重大变化的部分是,要使此工作正常进行,React将在捕获错误后在堆栈中重新执行上面某些React函数和React类构造函数的部分。由于渲染函数和类构造函数不应具有副作用(这对于服务器渲染也很重要),因此这不应引起任何实际问题。
删除私有导出
最后,最后一个值得注意的重大变化是我们删除了一些以前暴露给其他项目的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测试库。
参考
React v17.0 Release Candidate: No New Features