第 1 章 React 16 的新增功能

第 1 章 React 16 的新增功能

Flying
2018-07-10 / 0 评论 / 325 阅读 / 正在检测是否收录...

React 16 的发布包含了足够多的重要更改,可以用一章来描述它们。这一特定版本的发布时间相对较长。这是因为 React 的内部协调是从根本上重写的,它决定了如何有效地渲染组件更改。兼容性是另一个因素:这次重写没有重大的 API 更改。

在本章中,你将了解 React 16 中引入的主要变化:

  • 对协调内部进行的主要更改,它们对 React 项目的意义以及发展方向
  • 通过设置错误边界将错误限制到应用的各个部分
  • 创建渲染多个元素的组件和渲染字符串的组件
  • 渲染到 Portal

重新思考渲染

你不需要深入了解 React 的协调内部工作原理。这将破坏 React 的目的,以及它如何为我们封装所有这些工作。然而,了解 React 16 中发生的重大内部变化的动机,以及它们在更高层次上的工作方式,将有助于思考如何为当前和未来的 React 应用最好地设计组件。

现状

在选择帮助构建用户界面的库时,React 已将自己确立为标准之一。实现这一点的两个关键因素是它的简单性和性能。React 很简单,它的 API 很少,很容易上手和实验。React 是高性能的,因为它通过协调渲染树中的更改,最大限度地减少了必须调用的 DOM 操作的数量。

这两个因素之间的相互作用促成了 React 的流行。如果 API 难以使用,React 提供的良好性能将毫无价值。React 的首要价值是它易于使用,并且开箱即用。

随着 React 的广泛使用,人们意识到其内部协调机制可以得到改善。例如,一些 React 应用更新组件状态的速度快于渲染完成的速度。来看另一个示例:对屏幕上不可见的渲染树部分的更改应该比用户可以看到的元素具有更低的优先级。 像这样的问题足以降低用户体验,以至于感觉不像它应该的那样流畅。

如何在不中断 API 的情况下解决这些问题并呈现运行良好的树协调呢?

运行到完成

JavaScript 运行到完成是单线程的。这意味着默认情况下,你运行的任何 JavaScript 代码都将阻止任何其他浏览器任务运行,例如绘制屏幕。这就是为什么 JavaScript 代码的速度特别重要。然而,在某些情况下,即使 React 协调代码的性能也不足以掩盖用户的瓶颈。当渲染新树时,React 别无选择,只能在计算新的渲染树时阻止 DOM 更新和事件侦听器。

一种可能的解决方案是将协调工作分解为更小的块,并以防止 JavaScript 运行到完成线程阻塞重要的 DOM 更新的方式进行排列。这意味着协调器不必渲染完整的树,然后又必须重新做一遍,因为在第一次渲染时发生了一个事件。

让我们来看看这个问题的一个直观示例:

blocked.jpg

该图显示了当 React 组件中的状态发生变化时,在渲染完成之前,其他任何事情都不会发生。正如你所看到的,随着状态变化的堆积,协调整个树可能会变得昂贵,同时,DOM 始终无法执行任何操作。

协调渲染树与 JavaScript 的运行到完成语义是同步的。换言之,React 无法暂停它正在做的让 DOM 更新的事情。现在让我们看看 React 16 是如何试图改变上图的:

update.jpg

该版本的 React 渲染/协调过程与上一版本类似。事实上,左边的组件没有任何变化——这反映了 React 16 中不变的 API。尽管存在一些细微但重要的差异。

让我们先看看协调器。它不是在每次组件更改状态时都构建新的渲染树,而是渲染一颗部分树。换句话说,它执行大量工作,从而创建部分渲染树。它没有完成整个树的原因是协调过程可以暂停并允许运行任何 DOM 更新——你可以在图像的右侧看到 DOM 中的差异。

当协调器恢复构建渲染树时,它首先检查自暂停以来是否发生了新的状态更改。如果是这样,它将获取部分完成的渲染树,并根据新的状态更改重用它所能使用的资源。然后,它一直持续到下一个暂停。最终,协调完成。在协调过程中,DOM 有机会响应事件并呈现任何未完成的更改。在 React 16 之前,这是不可能的。你必须等到整棵树都被渲染后,DOM 中的任何事情才会发生。

什么是 Fiber?

为了将渲染组件的工作划分为更小的工作单元,React 创建了一个称为 Fiber 的抽象。Fiber 表示可以暂停和恢复的渲染工作单元。它还具有其他低级属性,例如优先级和 Fiber 输出完成后应返回的位置。

React 16 在开发期间的代号是 React Fiber,因为这种基本的抽象可以调度整体渲染工作的各个部分,以提供更好的用户体验。 React 16 标志着这个新的协调架构的初始版本,但还没有完成。 例如,一切仍然是同步的。

异步和未来之路

React 16 为下一个主要版本中异步渲染的最终目标奠定了基础。React 16 中未包含此功能的主要原因是团队希望将基本的协调变化付诸实践。还有一些其他的新功能需要发布,我们将在下面的章节中讨论。

一旦将异步渲染功能引入到 React 中,你就不必修改任何代码。相反,你可能会注意到应用某些领域的性能有所提高,这得益于优先级和定时渲染。

更好的组件错误处理

React 16 为组件引入了更好的错误处理能力。这个概念被称为错误边界,它被实现为一个生命周期方法,当任何子组件抛出异常时调用该方法。实现 componentDidCatch() 的父类是错误边界。你可以在整个应用中有不同的边界,具体取决于你的功能的组织方式。

此功能是为了是让应用有机会从某些错误中恢复。在 React 16 之前,如果组件抛出错误,整个应用将停止。这可能不太理想,尤其是当次要组件的问题使关键组件无法工作时。

让我们创建一个带有错误边界的 App 组件:

class App extends Component {
  state = {};
  componentDidCatch(err) {
    this.setState({ err: err.message });
  }
  render() {
    return (
      <p>
        <MyError err={this.state.err} />
      </p>
    );
  }
}

App 组件只渲染 MyError —— 一个故意抛出错误的组件。发生这种情况时,将以错误作为参数调用 componentDidCatch() 方法。然后可以使用该值来更改组件的状态。在此示例中,它将错误消息设置为错误状态。然后,App 将尝试重新渲染。

正如你所看到的,this.state.err 作为属性传递给 MyError。在第一次渲染期间,该值未定义。当 App 捕捉到 MyError 抛出的错误时,错误会传回组件。现在让我们看看 MyError

const MyError = (props) => {
  if (props.err) {
    return <b style={{ color: 'red' }}>{props.err}</b>;
  }
  throw new Error('epic fail');
};

该组件抛出一个消息为epic fail的错误。当 App 捕捉到此错误时,它会使用 err 属性渲染 MyError。当这种情况发生时,它只是将错误字符串渲染为红色。这恰好是我为这个应用选择的策略;在再次调用错误行为之前始终检查错误状态。在 MyError 中,通过第二次不执行 throw new Error('epic fail')来恢复整个应用。

使用 componentDidCatch(),你可以自由设置任何你喜欢的错误恢复策略。但通常情况下,你无法恢复发生故障的特定组件。

渲染多个元素和字符串

自 React 首次发布以来,规则是组件只能渲染一个元素。这在 React 16 中有两个重要的变化。首先,你现在可以从组件返回元素集合。这大大简化了渲染兄弟元素的情况。其次,现在可以渲染纯文本内容。

这两种更改都会导致页面上的元素减少。通过允许由组件渲染同级元素,你不必为了返回单个元素而用元素包装它们。通过渲染字符串,你可以将测试内容渲染为子组件或其他组件,而无需将其包装在元素中。

下面是渲染多个元素的样子:

const Multi = () =>
  ['first sibling', 'second sibling'].map((v, i) => <p key={i}>{v}</p>);
注意:必须为集合中的的元素提供 key 属性。现在,让我们添加一个返回字符串值的元素:
const Label = () => 'Name:';
const MultiWithString = () =>
  ['first sibling', 'second sibling'].map((v, i) => (
    <p key={i}>
      <Label /> {v}
    </p>
  ));

Label 组件只是返回一个字符串作为其呈现的内容。p 元素呈现 Label 作为子元素,与 {v} 值相邻。 当组件可以返回字符串时,你可以通过更多选项来组合构成 UI 的元素。

渲染到 Portal

我想介绍的 React 16 的最后一个新特性是 Portal 的概念。通常,组件的渲染输出放置在树中 JSX 元素所在的位置。然而,有时我们可以更好地控制组件渲染输出的最终位置。例如,如果你想在根 React 元素之外渲染组件,该怎么办呢?

Portal 允许组件在渲染时指定其容器元素。假设你希望在应用中显示通知。屏幕上不同位置的几个组件需要能够在屏幕上的一个特定位置渲染通知。让我们来看看如何使用 Portal 来定位元素:

import React, { Component } from 'react';
import { createPortal } from 'react-dom';

class MyPortal extends Component {
  constructor(...args) {
    super(...args);
    this.el = document.createElement('strong');
  }
  componentWillMount() {
    document.body.appendChild(this.el);
  }
  componentWillUnmount() {
    document.body.removeChild(this.el);
  }
  render() {
    return createPortal(this.props.children, this.el);
  }
}

在该组件的构造器方法中,目标元素被创建并存储在 el 属性中。然后,在 componentWillMount() 中,元素被附加到文档主体。你实际上不需要在组件中创建目标元素——你可以使用现有元素。你可以在 componentWillUnmount() 方法中删除此元素。

render() 方法中,createPortal() 函数用于创建 Portal。它有两个参数:要渲染的内容和目标 DOM 元素。在本例中,传递的是 MyPortal 子属性。让我们来看看 MyPortal 是如何使用的:

class App extends Component {
  render() {
    return (
      <div>
        <p>Main content</p>
        <MyPortal>Bro, you just notified me!</MyPortal>
      </div>
    );
  }
}

最终结果是传递给 MyPortal 的文本被渲染为根 React 元素之外的 strong 元素。在 Portal 出现 之前,你必须求助于某种命令式的解决方法才能使此类工作正常工作。现在,可以在需要它的同一上下文中在需要的相同上下文中渲染通知——它恰好被插入到 DOM 的其他位置以便正确显示。

总结

本章的目的是向你介绍 React 16 中的重大变化。值得注意的是,与之前的 React 版本几乎没有兼容性问题。这是因为大多数更改都是内部的,不需要更改 API。还增加了一些新功能。

React 16 的最大变化是其新的协调内部机制。不再试图在组件更改状态时协调所有内容,而是将协调工作分解为更小的单元。 可以对这些单元进行优先排序、安排、暂停和恢复。 在不久的将来,React 将充分利用这种新架构并开始异步渲染工作单元。

你还学习了如何在 React 组件中使用新的错误边界功能。使用错误边界允许你在不关闭整个应用的情况下从组件错误中恢复。然后,你了解到 React 组件现在可以返回组件集合。这就像渲染组件集合时一样。现在你可以直接从组件执行此操作。最后,你学习了如何使用 Portal 将组件渲染到非标准位置。

下一章中你将学习如何构建响应式组件。

9

评论 (0)

取消