共计 7804 个字符,预计需要花费 20 分钟才能阅读完成。
前言
半个月前 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>
无关具体装置阐明,请参阅文档。