React v17.0候选发布版的介绍

96 阅读19分钟

今天,我们将发布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 事件不再冒泡,以防止常见的混淆
  • ReactonFocusonBlur 事件已经切换到使用引擎盖下的原生focusinfocusout 事件,这与 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只对类和函数组件做这个处理,但没有检查forwardRefmemo 组件的返回值。这是由一个编码错误造成的。

在React 17中,forwardRefmemo 组件的行为与普通函数和类组件一致。从它们返回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-runtimereact/jsx-dev-runtime
  • 从本地错误框架构建组件堆栈。
  • 允许在上下文中指定displayName ,以改进堆栈。
  • 防止'use strict' 在UMD捆绑中的泄漏。
  • 停止在重定向中使用fb.me

React DOM

  • 将事件委托给根而不是document
  • 在运行任何下一个效果之前,清理所有效果。
  • 异步运行useEffect 清理函数
  • 使用浏览器focusinfocusout 来处理onFocusonBlur.
  • 使所有的Capture 事件使用浏览器的捕获阶段。
  • 不要模仿onScroll 事件的冒泡现象。
  • 如果forwardRefmemo 组件返回undefined ,则抛出。
  • 移除事件池。
  • 停止暴露那些React Native Web不需要的内部结构。
  • 当根部挂载时,附加所有已知的事件监听器
  • 在DEV模式双倍渲染的第二次渲染过程中禁用console
  • 废弃没有记录的和误导性的ReactTestUtils.SimulateNative API。
  • 重命名内部使用的私有字段名。
  • 不要在开发中调用用户计时API。
  • 在严格模式下重复渲染时禁用控制台。
  • 在严格模式下,没有Hooks的组件也可以重复渲染。
  • 允许在生命周期方法中调用ReactDOM.flushSync (但要警告)。
  • 在键盘事件对象中添加code 属性。
  • video 元素添加disableRemotePlayback 属性。
  • input 元素添加enterKeyHint 属性。
  • 当没有提供value<Context.Provider> 时发出警告。
  • memoforwardRef 组件返回undefined 时发出警告。
  • 改进无效更新的错误信息。
  • 从堆栈框架中排除forwardRef和memo。
  • 改进在受控和非受控输入之间切换时的错误信息。
  • 保持onTouchStart,onTouchMove, 和onWheel 的被动性。
  • 修复setState 在封闭的iframe内挂起的开发。
  • 修复使用defaultProps 的懒惰组件的渲染保释。
  • 修正当dangerouslySetInnerHTMLundefined 时的错误警告
  • 修复测试Utils与非标准的require 实现。
  • 修复onBeforeInput 报告不正确的event.type
  • 修复event.relatedTarget 在Firefox中报告为undefined
  • 修复IE11中的 "未指明的错误"。
  • 修复渲染成影子根的问题。
  • 修复movementX/Y polyfill与捕获事件。
  • onSubmitonReset 事件使用授权。
  • 改善内存的使用。

React DOM服务器

  • 使useCallback 的行为与服务器渲染器的useMemo 保持一致。

React测试渲染器

  • 改进findByType 错误信息。

并发模式(实验性)

  • 修改优先级批处理的启发式方法。
  • 在实验性API前添加unstable_ 的前缀
  • 删除unstable_discreteUpdatesunstable_flushDiscreteUpdates
  • 删除timeoutMs 的参数。
  • 禁用<div hidden /> prerendering,转而使用未来不同的API。
  • 为CPU绑定的树添加unstable_expectedLoadTime 到Suspense。
  • 添加一个实验性的unstable_useOpaqueIdentifier Hook。
  • 添加一个实验性的unstable_startTransition API。
  • 对CPU暂停使用全局渲染超时。
  • 在挂载前清除现有的根内容
  • 修复了一个错误边界的问题。
  • 修复了一个在悬空树中导致更新丢失的bug。
  • 修复了一个导致渲染阶段更新丢失的bug。
  • 修复SuspenseList中的一个bug。
  • 修复了一个导致Suspense fallback过早显示的bug。
  • 修正SuspenseList中的类组件的错误。
  • 修复了一个输入的bug,可能会导致更新被放弃。
  • 修复了一个导致暂停回退卡住的bug。
  • 如果水化,不要切掉SuspenseList的尾巴。
  • 修复了useMutableSource 中的一个bug,当getSnapshot 发生变化时,可能会发生这种情况。
  • 修复了useMutableSource 中的一个撕裂的bug。
  • 如果在渲染之外但在提交之前调用setState会有警告。