半个月前Vue 3.0刚刚发布了rc版本,React就紧随其后发布了rc版本。javascript
不过相比于Vue3对Vue2.x能力的巨大提高,React17对React16.x好像并无什么很给力的更新。html
在GitHub上的reactjs/reactjs.org文档中甚至出现了这样一句话:
没有任何新特性!这届React有点皮啊!java
那它到底更新了个啥呢?我们来把这个文档翻译一下看看: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至今已经有两年半的时间了,按照咱们的标准,时间跨度有些长了!在此篇博客中,咱们将讲解这次主要版本对你的影响以及如未尝试它。git
React 17不太寻常,由于它没有添加任何面向开发人员的新功能,而主要侧重于升级简化React自己。github
咱们正在积极开发React的新功能,但它们并不属于此版本。React 17是咱们进行深度推广战略的关键所在。web
此版本之因此特殊,你能够认为React 17是一个过渡版,它会使得由一个React版本管理树嵌入到另外一个React版本管理树中时会更加安全。shell
在过去七年的时间里,React一直遵循着all-or-nothing的升级策略。你能够继续使用旧版本,也能够将整个应用程序升级至新版本。但没有介于二者之间的状况。npm
此方式一直延续至今,但咱们确遭遇了all-or-nothing升级策略的局限性。许多API的变动,例如,反对使用 legacy context API时,并不能以自动化的方式来完成。至今可能大多数应用程序从未使用过它们,但咱们仍然选择在React中支持它们。咱们必须在无限期支持过期的API或针对某些应用仍使用旧版本 React 间进行选择。但这两个方案都不合适。react-native
所以,咱们想提供另外一种方案。
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 react@17.0.0-rc.0 react-dom@17.0.0-rc.0
经过 yarn 安装 React 17 RC 版,请执行:
yarn add react@17.0.0-rc.0 react-dom@17.0.0-rc.0
咱们还经过CDN提供了React RC的UMD构建版本:
<script crossorigin src="https://unpkg.com/react@17.0.0-rc.0/umd/react.production.min.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17.0.0-rc.0/umd/react-dom.production.min.js"></script>
有关详细安装说明,请参阅文档。