前言
半个月前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个。
对标浏览器
咱们对事件零碎进行了一些较小的更新:
- onScroll事件不再冒泡,以防止出现一些混同。
- React的onFocus和onBlur事件已在底层切换为原生的focusin和focusout事件。它们更靠近React现有行为,有时还会提供额定的信息。
- 捕捉事件(例如,onClickCapture)当初应用的是理论浏览器中的捕捉监听器。
这些更改会使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插件(请确保在我的项目中应用它)会对此状况收回正告。
返回统一的undefined谬误
在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>
无关具体装置阐明,请参阅文档。