React 16,是React的一个重大版本更新,发布于2017年9月。它,带来了很多,令人兴奋的新特性,比如,Fiber架构、错误边界、Portals、片段、支持返回字符串和数字、自定义DOM属性、setState返回null取消更新、生命周期函数的更新等。

这些新特性,让React,更强大,更灵活,性能也更好。但是,如何正确地使用这些新特性,发挥它们的最大价值,避免踩坑?

我在几个项目中,深度使用了React 16,积累了一些经验。今天,就把这些经验,总结一下,分享给大家。

一、Fiber架构:理解它,但不要过度依赖

Fiber,是React 16的,新的核心架构。它,是对React核心算法的,一次重写。

Fiber之前,React的渲染过程,是同步的,不可中断的。一旦开始渲染,就会,一直执行下去,直到完成。如果,组件树很大,渲染时间很长,就会,阻塞主线程,导致,页面卡顿,用户输入无响应。

Fiber,把渲染过程,拆分成了,一个个小的任务单元(Fiber)。它,可以,把渲染任务,分成多个帧来执行,每一帧,执行一部分任务,然后,把控制权,交还给浏览器,让浏览器,响应用户输入,执行动画等。这样,就不会,长时间阻塞主线程,页面,会更流畅。

而且,Fiber,还支持,任务的优先级。不同的更新,可以有不同的优先级。高优先级的更新(比如,用户输入),可以,打断低优先级的更新(比如,数据加载),优先执行。这样,用户体验,会更好。

最佳实践:

  1. 理解Fiber的原理,但不要过度依赖它的内部实现。 Fiber,是React的内部实现,它的API,可能会变化。你应该,理解它的原理,知道它能做什么,但是,不要,依赖它的内部实现细节。
  1. 利用Fiber带来的性能提升,但不要,为了优化而优化。 Fiber,已经让React的性能,好了很多。大多数情况下,你不需要,做额外的优化。只有,当你,确实遇到了性能问题,才需要,去优化。
  1. 注意,Fiber下,生命周期函数的调用时机,可能和之前不同。 因为,Fiber的渲染过程,是可中断的,所以,某些生命周期函数,可能会被调用多次。这一点,在后面,会详细讲。

二、错误边界(Error Boundaries):优雅地处理错误

错误边界,是React 16,引入的,一个新特性。它,可以,捕获子组件树中,发生的JavaScript错误,并且,渲染出,备用的UI,而不是,让整个组件树,崩溃。

在React 16之前,组件中的JavaScript错误,会导致,整个应用,崩溃,白屏。用户,只能,刷新页面,才能恢复。这,体验很差。

错误边界,解决了这个问题。你可以,创建一个错误边界组件,用它,包裹,可能出错的子组件。当子组件出错时,错误边界,会捕获错误,然后,渲染出,备用的UI,比如,"出错了,请稍后重试"。

创建错误边界,很简单。你只需要,在组件中,定义一个,componentDidCatch 生命周期函数,或者,一个静态的 getDerivedStateFromError 方法。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.error('Error:', error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>出错了,请稍后重试</h1>;
    }
    return this.props.children;
  }
}

使用的时候,用它,包裹子组件:

<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

最佳实践:

  1. 在合适的地方,使用错误边界。 不是,所有的组件,都需要,错误边界。你应该,在,可能出错的地方,使用错误边界。比如,第三方组件,数据展示组件,用户输入组件等。
  1. 错误边界,要分层。 不要,只在应用的最顶层,放一个错误边界。这样,一个小错误,就会导致,整个应用,显示错误页面。你应该,在不同的层级,放置不同的错误边界。比如,每个页面,一个错误边界;每个复杂的组件,一个错误边界。这样,一个地方出错,只会影响,那个地方,其他地方,还能正常使用。
  1. 错误边界,要记录错误日志。componentDidCatch 中,你应该,把错误信息,记录下来,发送到,日志服务器。这样,你才能,知道,用户遇到了什么错误,及时修复。
  1. 错误边界,不能捕获,所有的错误。 错误边界,只能捕获,子组件,在渲染过程中,发生的错误。它,不能捕获,事件处理器中的错误,异步代码中的错误,服务端渲染中的错误,以及,错误边界自身的错误。这些错误,你需要,用其他方式,来处理。

三、Portals:把组件渲染到任何地方

Portals,是React 16,引入的,另一个新特性。它,可以,把组件,渲染到,DOM树中的,任何位置,而不是,只能渲染到,父组件的DOM节点中。

这对于,模态框、弹窗、下拉菜单、提示框等,需要,脱离父容器的,组件,非常有用。

在React 16之前,要实现,模态框等组件,通常,需要,用CSS的 position: fixed,来定位。但是,如果,父组件,有 transformfilterperspective 等属性,会影响,position: fixed 的定位,导致,模态框,位置错乱。

Portals,解决了这个问题。你可以,把模态框的内容,通过Portal,渲染到,document.body 下,这样,就不会,受到父组件的影响了。

使用Portals,很简单。你只需要,调用 ReactDOM.createPortal(child, container),其中,child 是,要渲染的React元素,container 是,目标DOM节点。

class Modal extends React.Component {
  render() {
    return ReactDOM.createPortal(
      <div className="modal">
        {this.props.children}
      </div>,
      document.body
    );
  }
}

最佳实践:

  1. 用Portals,实现模态框、弹窗、下拉菜单等组件。 这些组件,通常,需要,脱离父容器,用Portals,是最好的选择。
  1. 注意,Portals的事件冒泡。 虽然,Portal的内容,渲染到了,DOM树的其他位置,但是,它的事件,仍然会,按照React组件树,冒泡。也就是说,Portal的父组件,仍然能,接收到,Portal内容的事件。这一点,要注意。
  1. 用Portals时,要管理好,目标DOM节点。 你需要,确保,目标DOM节点,存在,并且,在组件卸载时,清理好,Portal渲染的内容。通常,你可以,在组件挂载时,创建一个div,添加到document.body,然后,把Portal渲染到这个div中。组件卸载时,把这个div,移除掉。

四、片段(Fragments):避免多余的DOM节点

片段,是React 16,引入的,一个新特性。它,可以,让组件,返回多个元素,而不需要,包裹一个,多余的父元素。

在React 16之前,组件的render方法,只能返回,一个根元素。如果你,需要返回多个元素,就必须,用一个div,或者其他元素,把它们包裹起来。但是,这样,会在DOM中,增加,多余的节点,有时候,会影响,布局和样式。

React 16,支持了,片段。你可以,用 React.Fragment,或者,简写的 <>...</>,来包裹,多个元素。片段,不会,在DOM中,渲染出,任何额外的节点。

// 用React.Fragment
render() {
  return (
    <React.Fragment>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </React.Fragment>
  );
}

// 用简写语法
render() {
  return (
    <>
      <li>Item 1</li>
      <li>Item 2</li>
      <li>Item 3</li>
    </>
  );
}

而且,React 16,还支持,直接返回,字符串和数字,不需要,包裹在元素中。

render() {
  return 'Hello World';
}

最佳实践:

  1. 尽可能,使用片段,避免多余的DOM节点。 当你,需要返回多个元素,但是,不需要,额外的父容器时,用片段。
  1. 注意,简写语法 <>...</>,不支持,key和其他属性。 如果你,需要,给片段,加key(比如,在列表中),你必须,用 React.Fragment,而不是,简写语法。
// 正确
{items.map(item => (
  <React.Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.description}</dd>
  </React.Fragment>
))}

// 错误,简写语法不支持key
{items.map(item => (
  <>
    <dt>{item.term}</dt>
    <dd>{item.description}</dd>
  </>
))}
  1. 不要,滥用片段。 片段,虽然好,但是,也不是,所有地方,都要用。如果,你需要,一个父容器,来设置样式,或者,布局,那就,用div,或者其他元素。片段,只是,用来,避免,多余的,不需要的DOM节点。

五、setState返回null:取消更新

React 16,支持了,在 setState 的更新函数中,返回 null,来取消,这次更新。

在React 16之前,如果你,在 setState 中,计算出新的state,和当前的state,一样,React,还是会,触发,一次重新渲染。虽然,React,会在渲染时,对比,发现,没有变化,然后,不更新DOM,但是,渲染的过程,还是会执行,会有,一定的性能开销。

React 16,你可以,在 setState 的更新函数中,返回 null,来告诉React,这次,不需要更新。这样,React,就会,直接跳过,这次更新,不会,触发重新渲染。

this.setState(prevState => {
  if (prevState.count === newCount) {
    return null; // 取消更新
  }
  return { count: newCount };
});

最佳实践:

  1. 在,可能不需要更新的场景,使用这个特性。 比如,你,根据props,计算state,如果,计算结果,和当前state,一样,就返回null,取消更新。
  1. 不要,过度使用。 大多数情况下,React的性能,已经足够好了。你不需要,在每个setState中,都去判断,要不要返回null。只有,当你,确实遇到了,性能问题,或者,有明确的,不需要更新的场景,才使用。

六、生命周期函数的更新:注意这些变化

React 16,对一些生命周期函数,做了,更新和废弃。你需要,注意这些变化,避免,踩坑。

1. componentWillMount、componentWillReceiveProps、componentWillUpdate,被标记为不安全。

这三个生命周期函数,在React 16中,被标记为,不安全(UNSAFE_)。因为,在Fiber架构下,这三个函数,可能会,被调用多次,而且,它们,经常被,误用,导致,bug。

React,推荐,用以下方式,替代它们:

  • componentWillMount → 用 constructor 或者 componentDidMount 替代
  • componentWillReceiveProps → 用 getDerivedStateFromProps 替代
  • componentWillUpdate → 用 getSnapshotBeforeUpdate 替代

2. 新增了 getDerivedStateFromProps 和 getSnapshotBeforeUpdate。

getDerivedStateFromProps,是一个静态方法,在,每次渲染前,调用。它,接收,当前的props和state,返回,新的state,或者null。它,用来,替代 componentWillReceiveProps,根据props的变化,更新state。

getSnapshotBeforeUpdate,在,DOM更新前,调用。它,接收,之前的props和state,返回,一个值,这个值,会作为,第三个参数,传给 componentDidUpdate。它,用来,替代 componentWillUpdate,在DOM更新前,获取一些信息。

最佳实践:

  1. 尽快,迁移到,新的生命周期函数。 虽然,旧的生命周期函数,在React 16中,还能使用,但是,在未来的版本中,会被移除。你应该,尽快,迁移到,新的生命周期函数。
  1. 注意,getDerivedStateFromProps,是静态方法,不能访问this。 这意味着,你,不能在里面,调用,实例的方法,或者,访问,实例的属性。你,只能,根据,传入的props和state,来计算,新的state。
  1. 不要,滥用getDerivedStateFromProps。 很多时候,你,不需要,用它,来根据props更新state。你应该,优先考虑,用,完全受控组件,或者,完全不受控组件,来替代。只有,当你,确实需要,根据props,更新state时,才用它。

七、写在最后

以上,就是我,在实际项目中,使用React 16新特性的,一些经验和最佳实践。当然,React 16的新特性,还有很多,比如,自定义DOM属性、服务端渲染的改进、流式渲染等,我就不一一介绍了。

React 16,是一个,非常重要的版本。它,不仅,带来了,很多新特性,更重要的是,它,为React的未来,打下了,坚实的基础。Fiber架构,让React,有了,无限的可能性。未来,React,会在,Fiber的基础上,推出,更多,令人兴奋的特性。

作为前端开发者,我们,应该,及时,学习和掌握,这些新特性,并且,在实际项目中,合理地使用它们,提升,开发效率,和用户体验。

希望这篇文章,能给大家,带来一些帮助。如果你,有其他,React 16的使用经验,或者,最佳实践,欢迎在评论区留言,我们一起交流。