半个月前Vue 3.0刚刚发布了rc版本,React就紧随其后发布了rc版本。
不过相比于Vue3对Vue2.x能力的巨大提升,React17对React16.x好像并没有什么很给力的更新。
在GitHub上的reactjs/reactjs.org文档中甚至出现了这样一句话:
没有任何新特性!这届React有点皮啊!
那它到底更新了个啥呢?咱们来把这个文档翻译一下看看:
文档地址:https://github.com/reactjs/reactjs.org/blob/c30ff1e39b9fca747198c028a33300656a90e612/content/blog/2020-08-10-react-v17-rc.md
标题 | 作者 |
---|---|
React 17.0 : 没有新特性 | gaearon rachelnabors |
今天,我们发布了React v17的第一个 RC 版本。自上一个主要版本的React至今已经有两年半的时间了,按照我们的标准,时间跨度有些长了!在此篇博客中,我们将讲解此次主要版本对你的影响以及如何尝试它。
React 17不太寻常,因为它没有添加任何面向开发人员的新功能,而主要侧重于升级简化React本身。
我们正在积极开发React的新功能,但它们并不属于此版本。React 17是我们进行深度推广战略的关键所在。
此版本之所以特殊,你可以认为React 17是一个过渡版,它会使得由一个React版本管理树嵌入到另一个React版本管理树中时会更加安全。
在过去七年的时间里,React一直遵循着all-or-nothing的升级策略。你可以继续使用旧版本,也可以将整个应用程序升级至新版本。但没有介于两者之间的情况。
此方式一直延续至今,但我们确遭遇了all-or-nothing升级策略的局限性。许多API的变更,例如,反对使用 legacy context API时,并不能以自动化的方式来完成。至今可能大多数应用程序从未使用过它们,但我们仍然选择在React中支持它们。我们必须在无限期支持过时的API或针对某些应用仍使用旧版本 React 间进行选择。但这两个方案都不合适。
因此,我们想提供另一种方案。
React 17开始支持逐步升级React版本。当从React 15升到16时(或者从 React 16升到17时),通常会一次升级整个应用程序。这适用于大部分应用程序。但是,如果代码库是在几年前编写的,并且并没有得到很好的维护,那么升级它会变得越来越有挑战性。尽管可以在页面上使用两个版本的React,但是直到React 17依然会出现events问题。
我们使用React 17解决了许多诸如此类的问题。这将意味着当React 18或未来版本问世时,你将有更多选择。首选还是像以前一样,一次升级整个应用程序。但你也可以选择逐步升级你的应用程序。例如,你可能会将大部分应用程序迁移至React 18,但在React 17上保留一些延迟加载的对话框或子路由。
但这不意味着你必须逐步升级。对于大部分应用程序来说,一次性全部升级仍是最好的解决方案。加载两个React版本,即使其中一个是按需延迟加载的,仍然不太理想。但是,对于没有积极维护的大型应用来说,可以考虑此种方案,并且 React 17开始可以保证这些应用程序不落伍。
为了实现逐步升级,我们需要对React的事件系统进行一些更改。而这些更改可能会对代码产生影响,这也是React 17成为主要版本的原因。实际上,十万个以上的组件中受影响的组件不超过20个,因此我们希望大多数应用程序都可以升级到React 17,而不会产生太多影响。如果遇到问题的话可以联系我们。
我们准备了一个示例(GitHub)仓库,展示了如何在必要时延迟加载旧版本的React。该示例使用了Create React App进行构建,但对其他工具采用类似的方法应该也适用。我们欢迎使用其他工具的开发者编写demo并提交pr。
注意: 我们已将其他的更新推迟到React 17之后。此版本的目标是实现逐步升级。如果升级React 17太困难的话,我们的目标会无法实现。
从技术上讲,始终可以在应用程序中嵌套不同版本的React。但由于React事件系统的工作原理导致很难实现。
在React组件中,通常会内联编写事件处理:
<button onClick={handleClick}>
与此代码等效的DOM操作如下:
myButton.addEventListener('click', handleClick);
但对大多数事件来说,React并不会将它们附加到DOM节点上。相反,React会直接在document节点上为每种事件类型附加一个处理器,这被称为事件委托。除了在大型应用程序上具有性能优势外,它还使添加类似于replaying events这样的新特性变得更容易。
自从其发布以来,React就一直自动进行事件委托。当document上触发DOM事件时,React会找出调用的组件,然后 React事件会在组件中向上"冒泡"。但实际上,原生事件已经冒泡出了"document"级别,React是在document中安装的事件处理器。
但这就是逐步升级的困难所在。
如果页面上有多个React版本,他们都将在顶层注册事件处理器。这会破坏e.stopPropagation() 如果嵌套树结构中阻止了事件冒泡,但外部树依然能接收到它。这会使不同版本React的嵌套变得十分困难。这种担忧并不是没有根据的 —— 例如,四年前Atom编辑器就遇到了相同的问题。
这也是我们为什么要改变React底层附加事件方式的原因。
在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树的嵌套。请注意,要使其正常工作,两个版本都必须为17或更高版本,这就是为什么强烈建议升级到React 17的根本原因。从某种意义上讲,React 17是一个过渡版本,使逐步升级成为可能。
此更改还让React嵌入使用其他技术构建的应用程序变得更加容易。例如,如果应用程序的"外壳"是用jQuery编写的,但其中较新的代码是用React编写的,则React代码中的e.stopPropagation()会阻止它影响jQuery的代码 —— 就像你所期盼的那样。换句话说,如果你不再喜欢React并想重写应用程序(比如用jQuery),则可以从外层开始将 React转换为jQuery,而不会破坏事件冒泡。
经核实,多年来在issue tracker上报告的许多问题都已被新特性解决,这些问题大多都与将React与非React代码集成有关。
注意: 你可能想知道这是否会破坏根容器之外的Portals。答案是React还会监听portals容器上的事件,所以这不是问题。
与其他重大更新一样,可能需要对代码进行调整。在Facebook,我们在成千上万个模块中大约调整了十个模块以适应这次更新。
例如,如果模块中使用document.addEventListener(…)手动添加了DOM监听,你可能希望能捕获到所有React 事件。在React 16或更早版本中,即使你在React事件处理器中调用e.stopPropagation(),你创建的DOM监听仍会触发,这是因为原生事件已经处于document级别。使用React 17冒泡将被阻止(按需),因此你的document级别的事件监听不会触发:
document.addEventListener('click', function() {
// 如果React组件调用了e.stopPropagation()
// 那么这个自定义监听函数不会收到click事件
});
你可以将监听转换为使用事件捕获来修复此类代码。为此,你可以将{ capture: true }作为 document.addEventListener的第三个参数传递:
document.addEventListener('click', function() {
// 现在这个事件处理函数使用了事件捕获,
// 所以它可以接收到所有的点击事件!
}, { capture: true });
请注意,此策略在全局上具有更好的适应性。例如,它可能会修复代码中现有的错误,这些错误在 React 事件处理器外部调用 e.stopPropagation() 发生。换句话说,React 17的事件冒泡更接近常规DOM。
我们将 React 17中的重大更改保持在最低水平。例如,它不会删除以前版本中弃用的任务方法。但是,它的确包含一些其他重大更改,根据经验,这些更改会相对安全。总体而言,由于这些因素的存在,在十万个以上的组件中受影响的组件不超过20个。
我们对事件系统进行了一些较小的更新:
这些更改会使React与浏览器行为更接近,并提高了互操作性。
注意: 尽管从React 17把focus事件切换成了focusin,但onFocus并未影响冒泡行为。在React中,onFocus事件总是冒泡的,它在React 17中继续冒泡,因为通常它是一个更有用的默认值。查看这个sandbox,了解可以针对不同的特定用例添加的不同检查。
React 17中移除了"event pooling(事件池)"。它并不会提高现代浏览器的性能,甚至还会使经验丰富的开发者一头雾水:
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上造成影响(甚至还修复了一些bug!)。请注意,e.persist()在 React事件对象中仍然可用,只不过没有任何效果罢了。
我们正在使useEffect和清理函数的时机保持一致。
useEffect(() => {
// This is the effect itself.
return () => {
// This is its cleanup.
};
});
大多数副作用(effect)不需要延迟刷新视图,因此React在屏幕上反映出更新后立即异步执行它们(在极少数情况下,你需要一种副作用来阻止重绘。例如,如果需要获取尺寸和位置,请使用useLayoutEffect)。
然而,副作用清理函数(如果存在)在React16中同步运行。我们发现,对于大型应用程序来说,这不是理想选择,因为同步会减缓视图的更新(例如,切换标签)。
在React 17中,副作用清理函数会异步执行 —— 如果要卸载组件,则清理会在视图更新后运行。
这反映了副作用本身如何更紧密地运行。在极少数情况下,你可能希望依靠同步执行,可以改用useLayoutEffect来代替。
注意: 你可能想知道这是否意味着你现在将无法修复有关未挂载组件上的setState的警告。不必担心,React专门处理了这种情况,并且不会在卸载和清理之间短暂间隔内发出setState的警告。因此,取消代码的请求或间隔几乎总是可以保存不变的。
此外,React 17会根据它们在tree中的位置,以与效果相同的顺序执行cleanup。在以前的时候顺序有时会不同。
可复用的库可能需要对此情况进行深度测试,但我们只遇到了几个组件会因为此问题中断执行。比如:
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() {
// 这里忘记了写ruturn,所以这个组件返回了一个undefined。
// React会报错而不会忽略它。
<button />;
}
以前,React只对class和函数组件执行此操作,但并不会检查forwardRef和memo组件的返回值。这是由于编码错误导致。
在React 17中,forwardRef和memo组件的行为会与常规函数组件和class组件保持一致。在返回undefined时会报错
let Button = forwardRef(() => {
// 这里忘记了写ruturn,所以这个组件返回了一个undefined。
// React17会报错而不会忽略它。
<button />;
});
let Button = memo(() => {
// 这里忘记了写ruturn,所以这个组件返回了一个undefined。
// React17会报错而不会忽略它。
<button />;
});
对于不想进行任何渲染的情况,请return 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 [email protected] [email protected]
通过 yarn 安装 React 17 RC 版,请执行:
yarn add [email protected] [email protected]
我们还通过CDN提供了React RC的UMD构建版本:
<script crossorigin src="https://unpkg.com/[email protected]/umd/react.production.min.js">script>
<script crossorigin src="https://unpkg.com/[email protected]/umd/react-dom.production.min.js">script>
有关详细安装说明,请参阅文档。