今天,我们将发布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多个组件中的不到20个,所以我们预计大多数应用程序可以升级到React 17而不会有太大的麻烦。如果你遇到问题,请告诉我们。
逐步升级的演示
我们准备了一个例子库,演示如何在必要时偷懒加载旧版本的React。这个演示使用了Create React App,但应该可以用其他任何工具来进行类似的操作。我们欢迎使用其他工具的演示作为拉取请求。
注意
我们已经将其他变化推迟到React 17之后。这个版本的目的是为了实现逐步的升级。如果升级到React 17过于困难,那就违背了它的目的。
对事件委托的修改
从技术上讲,一直以来,用不同版本的React开发的应用程序都可以进行嵌套。然而,由于React事件系统的工作方式,它是相当脆弱的。
在React组件中,你通常在内联写事件处理程序:
<button onClick={handleClick}>
这个代码的vanilla 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(<App />, 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,而不会破坏事件传播。
我们已经确认,多年 来 在 我们的 问题 追踪器上 报告的与整合React和非React代码有关的许多 问题已经被新的行为所修复。
注意
你可能想知道这是否会破坏根容器之外的Portals。答案是,React也会监听门户容器上的事件,所以这不是一个问题。
修复潜在的问题
与任何破坏性的变化一样,很可能需要调整一些代码。在Facebook,我们总共要调整大约10个模块(在成千上万的模块中),以配合这一变化。
例如,如果你用document.addEventListener(...) 添加手动DOM监听器,你可能希望它们能捕捉所有的React事件。在React 16和更早的版本中,即使你在React事件处理程序中调用e.stopPropagation() ,你的自定义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 });
注意这个策略在整体上更有弹性--例如,它可能会修复你的代码中现有的错误,这些错误发生在React事件处理器之外调用e.stopPropagation() 。换句话说,React 17中的事件传播工作更接近于常规的DOM。
其他破坏性变化
我们已经把React 17中的破坏性变化保持在最低限度。例如,它没有删除任何在以前版本中被废弃的方法。然而,它确实包括一些其他的破坏性变化,根据我们的经验,这些变化是相对安全的。总的来说,在我们的100,000多个组件中,我们不得不因为它们而调整不到20个。
与浏览器保持一致
我们做了几个与事件系统有关的小改动:
onScroll事件不再冒泡,以防止常见的混淆。- React
onFocus和onBlur事件已经切换到使用引擎盖下的原生focusin和focusout事件,这与 React 的现有行为更接近,有时还提供额外的信息。 - 捕获阶段事件(例如
onClickCapture)现在使用真正的浏览器捕获阶段监听器。
这些变化使React与浏览器的行为更接近,并提高了互操作性。
注意
onFocus尽管React 17将focus改为focusin*,*但请注意,这并不影响冒泡行为。在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的警告。不要担心--React专门检查了这种情况,在卸载和清理之间的短暂间隙中,不会发出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 lint规则(请确保你使用它!)一直在警告这个问题。
返回未定义的一致的错误
在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只对类和函数组件做这个处理,但没有检查forwardRef 和memo 组件的返回值。这是由一个编码错误造成的。
在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.
<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组件堆栈,你必须在生产堆栈和包的大小之间做出选择。
在React 17中,组件堆栈是使用一种不同的机制生成的,将它们与常规的本地JavaScript堆栈连接起来。这让你在生产环境中获得完全符号化的React组件堆栈痕迹。
React实现这个的方式有点非正统。目前,浏览器没有提供一种方法来获得一个函数的堆栈框架(源文件和位置)。因此,当React捕捉到一个错误时,它现在将通过从上面的每个组件内部抛出(并捕捉)一个临时错误来重建其组件栈,如果可能的话。这为崩溃增加了一个小的性能惩罚,但它只在每个组件类型中发生一次。
如果你感到好奇,你可以在这个拉动请求中阅读更多细节,但在大多数情况下,这个确切的机制不应该影响你的代码。从你的角度来看,新功能是组件堆栈现在可以点击了(因为它们依赖于本地浏览器的堆栈框架),而且你可以在生产中对它们进行解码,就像你对普通的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候选发布版,并为你在迁移中可能遇到的问题提出任何问题。请记住,候选发布版比稳定版更有可能包含错误,所以先不要把它部署到生产中。
要用npm安装React 17 RC,请运行:
npm install react@17.0.0-rc.3 react-dom@17.0.0-rc.3
要用Yarn安装React 17 RC,请运行:
yarn add react@17.0.0-rc.3 react-dom@17.0.0-rc.3
我们还通过CDN提供React的UMD构建:
<script crossorigin src="https://unpkg.com/react@17.0.0-rc.3/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17.0.0-rc.3/umd/react-dom.production.min.js"></script>
更新日志
React
- 为新的JJSX转换添加
react/jsx-runtime和react/jsx-dev-runtime。 - 从本地错误框架构建组件堆栈。
- 允许在上下文中指定
displayName,以改进堆栈。 - 防止
'use strict'在UMD捆绑中的泄漏。 - 停止在重定向中使用
fb.me。
React DOM
- 将事件委托给根而不是
document。 - 在运行任何下一个效果之前,清理所有效果。
- 异步运行
useEffect清理函数 - 使用浏览器
focusin和focusout来处理onFocus和onBlur. - 使所有的
Capture事件使用浏览器的捕获阶段。 - 不要模仿
onScroll事件的冒泡现象。 - 如果
forwardRef或memo组件返回undefined,则抛出。 - 移除事件池。
- 停止暴露那些React Native Web不需要的内部结构。
- 当根部挂载时,附加所有已知的事件监听器
- 在DEV模式双倍渲染的第二次渲染过程中禁用
console。 - 废弃没有记录的和误导性的
ReactTestUtils.SimulateNativeAPI。 - 重命名内部使用的私有字段名。
- 不要在开发中调用用户计时API。
- 在严格模式下重复渲染时禁用控制台。
- 在严格模式下,没有Hooks的组件也可以重复渲染。
- 允许在生命周期方法中调用
ReactDOM.flushSync(但要警告)。 - 在键盘事件对象中添加
code属性。 - 为
video元素添加disableRemotePlayback属性。 - 为
input元素添加enterKeyHint属性。 - 当没有提供
value给<Context.Provider>时发出警告。 - 当
memo或forwardRef组件返回undefined时发出警告。 - 改进无效更新的错误信息。
- 从堆栈框架中排除forwardRef和memo。
- 改进在受控和非受控输入之间切换时的错误信息。
- 保持
onTouchStart,onTouchMove, 和onWheel的被动性。 - 修复
setState在封闭的iframe内挂起的开发。 - 修复使用
defaultProps的懒惰组件的渲染保释。 - 修正当
dangerouslySetInnerHTML是undefined时的错误警告 - 修复测试Utils与非标准的
require实现。 - 修复
onBeforeInput报告不正确的event.type。 - 修复
event.relatedTarget在Firefox中报告为undefined。 - 修复IE11中的 "未指明的错误"。
- 修复渲染成影子根的问题。
- 修复
movementX/Ypolyfill与捕获事件。 - 对
onSubmit和onReset事件使用授权。 - 改善内存的使用。
React DOM服务器
- 使
useCallback的行为与服务器渲染器的useMemo保持一致。
React测试渲染器
- 改进
findByType错误信息。
并发模式(实验性)
- 修改优先级批处理的启发式方法。
- 在实验性API前添加
unstable_的前缀 - 删除
unstable_discreteUpdates和unstable_flushDiscreteUpdates - 删除
timeoutMs的参数。 - 禁用
<div hidden />prerendering,转而使用未来不同的API。 - 为CPU绑定的树添加
unstable_expectedLoadTime到Suspense。 - 添加一个实验性的
unstable_useOpaqueIdentifierHook。 - 添加一个实验性的
unstable_startTransitionAPI。 - 对CPU暂停使用全局渲染超时。
- 在挂载前清除现有的根内容
- 修复了一个错误边界的问题。
- 修复了一个在悬空树中导致更新丢失的bug。
- 修复了一个导致渲染阶段更新丢失的bug。
- 修复SuspenseList中的一个bug。
- 修复了一个导致Suspense fallback过早显示的bug。
- 修正SuspenseList中的类组件的错误。
- 修复了一个输入的bug,可能会导致更新被放弃。
- 修复了一个导致暂停回退卡住的bug。
- 如果水化,不要切掉SuspenseList的尾巴。
- 修复了
useMutableSource中的一个bug,当getSnapshot发生变化时,可能会发生这种情况。 - 修复了
useMutableSource中的一个撕裂的bug。 - 如果在渲染之外但在提交之前调用setState会有警告。