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
事件不再冒泡以防止常见的混乱。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 17.0 Release Candidate并针对迁移中可能遇到的问题提出任何问题。请记住,候选版本比稳定版本更可能包含错误,因此请不要将其部署到生产环境中。
要使用npm安装React 17 RC,请运行:
npm install [email protected] [email protected]
要安装带有纱线的React 17 RC,请运行:
yarn add [email protected] [email protected]
我们还通过CDN提供了React的UMD构建:
有关详细的安装说明,请参阅文档。
react/jsx-runtime
和react/jsx-dev-runtime
用于新的JSX转换。(@lunaruan在#18299中)displayName
根据上下文指定改进堆栈。(@ eps1lon在#18224)'use strict'
UMD包中泄漏。(@ koba04在#19614)fb.me
用于重定向。(@cylim在#19598中)document
。(@trueadm在#18195及其他)useEffect
异步运行清除功能。(@bvaughn在#17925中)focusin
和focusout
用于onFocus
和onBlur
。(@trueadm在#19186)Capture
事件都使用浏览器捕获阶段。(@trueadm在#19221)onScroll
事件的冒泡。(@gaearon在#19464中)forwardRef
或memo
component返回undefined
。(@gaearon在#19550中)console
在DEV模式双渲染的第二个渲染通道中禁用。(@sebmarkbage在#18547)ReactTestUtils.SimulateNative
API。(@gaearon在#13407中)ReactDOM.flushSync
在生命周期方法中调用(但警告)。(@sebmarkbage在#18759)code
属性添加到键盘事件对象。(@ bl00mber在#18287)disableRemotePlayback
属性video
。(#18619中的@tombrowndev)enterKeyHint
属性input
。(@ eps1lon在#18634)value
提供时发出警告
。(@ charlie1404在#19054)memo
或forwardRef
组件返回时发出警告undefined
。(@bvaughn在#19550中)onTouchStart
,onTouchMove
和onWheel
被动。(@gaearon在#19654中)setState
在封闭的iframe中挂在开发中的问题。(@gaearon在#19220中)defaultProps
。(@jddxf在#18539中)dangerouslySetInnerHTML
的undefined
。(#18676中的@ eps1lon)require
实现修复测试实用程序。(@刚刚鲍里斯在#18632)onBeforeInput
报告错误的问题event.type
。(@ eps1lon在#19561)event.relatedTarget
报告为undefined
Firefox。(@claytercek在#19607)movementX/Y
使用捕获事件修复polyfill。(@gaearon在#19672中)onSubmit
和onReset
事件。(@gaearon在#19333中)useCallback
行为与useMemo
服务器渲染器一致。(#18783中的@alexmckenley)findByType
错误消息。(@henryqdineen在#17439)unstable_
在实验性API之前添加前缀。(@acdlite在#18825)unstable_discreteUpdates
和unstable_flushDiscreteUpdates
。(@trueadm在#18825)timeoutMs
参数。(@acdlite在#19703)
预渲染,以支持其他将来的API。(@acdlite在#18917中)unstable_expectedLoadTime
为CPU绑定树添加到Suspense。(#19936中的@acdlite)unstable_useOpaqueIdentifier
挂钩。(@lunaruan在#17322中)unstable_startTransition
API。(@rickhanlonii在#19696)act
在测试渲染器中使用不再刷新暂挂后备。(@acdlite在#18596)useMutableSource
可能在getSnapshot
更改时发生的错误。(@bvaughn在#18297)useMutableSource
。(@bvaughn在#18912中)