讲讲今后 React 异步渲染带来的生命周期变化

11,088 阅读5分钟

如果说你是一位经验丰富的 React 工程师,看到这边文章讲的是 React 的生命周期,已经看过无数篇关于 React 生命周期的文章的你,可能有关闭页面的想法。

请不要急着退出,我想讲一些不一样的、来自未来的。

2018.5.24 更新:

今天 React 16.4 发布了, getDerivedStateFromProps 的触发时机有所改动——原本在父组件重新渲染它时才会触发 getDerivedStateFromProps ,现改为只要渲染就会触发,包括自身 setState 。这一变化进一步地区分了该方法与 componentWillReceiveProps ,更适合异步渲染。

大家都知道,现在关于 React 生命周期的解析、教程、深入解读等文章早已数不胜数。那么为何我又要来重描一遍?因为 React 的生命周期即将改变 !React 团队目前致力于打造异步渲染的机制,以进一步提高 React 的渲染效能,在这一过程中,他们大刀阔斧地对生命周期做出了若干改革,以更好地适应异步渲染的需要。

简单说来,删除了 3 个生命周期方法,增加了另外 2 个:

+: 新增  -: 删除  ?: 有变化

- componentWillMount
  render
  componentDidMount
- componentWillReceiveProps
+ static getDerivedStateFromProps
  shouldComponentUpdate
- componentWillUpdate
+ getSnapshotBeforeUpdate
? componentDidUpdate
  componentWillUnmount
class Example extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    // 这一生命周期方法是静态的,它在组件实例化或接收到新的 props 时被触发
    // 若它的返回值是对象,则将被用于更新 state ;若是 null ,则不触发 state 的更新

    // 配合 `componentDidUpdate` 使用,这一方法可以取代 `componentWillReceiveProps`
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 该方法在实际改动(比如 DOM 更新)发生前的“瞬间”被调用,返回值将作为 `componentDidUpdate` 的第三个参数

    // 配合 `componentDidUpdate` 使用,这一方法可以取代 `componentWillUpdate`
  }

  componentDidUpdate(props, state, snaptshot) {
      // 新增的参数 snapshot 即是之前调用 getSnapshotBeforeUpdate 的返回值
  }
}

具体讲解

-componentWillMount

移除这一生命周期方法原因如下:

  1. 未来的异步渲染机制中,单个组件实例也可能多次调用该方法
  2. 它可以被分散到 constructorcomponentDidMount

举几个例子:

事件绑定、异步请求

部分经验不足的开发者可能误把 事件绑定 和 异步获取数据 的代码置于 componentWillMount 中,导致

a. 服务端渲染会触发这一生命周期方法,但因往往忽略异步获取的数据而白白请求 或 因服务端不触发 componentWillUnmount 而无法取消事件监听,导致内存泄漏
b. 结合 1. ,多次调用会导致发出重复的请求 或 进行重复监听,后者亦会导致内存泄漏

最佳实践:

class ExampleComponent extends React.Component {
  state = {
    subscribedValue: this.props.dataSource.value,
    externalData: null,
  };

  componentDidMount() {
    // 这里只触发一次,可以安全地进行 异步请求、事件绑定
    this.asyncRequest = asyncLoadData().then(
      externalData => {
        this.asyncRequest = null;
        this.setState({externalData});
      }
    );

    this.props.dataSource.subscribe(this.handleSubscriptionChange);
  }

  componentWillUnmount() {
    if (this.asyncRequest) this.asyncRequest.cancel();
    // 事件绑定在客户端才进行,且只进行一次,在这里可以安全地解绑
    this.props.dataSource.unsubscribe(this.handleSubscriptionChange);
  }

  handleSubscriptionChange = dataSource => {
    this.setState({ subscribedValue: dataSource.value });
  };
}

-componentWillReceiveProps & +getDerivedStateFromProps

来看一段常见的 componentWillReceiveProps 的用法:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
  };

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      // 检测到变化后更新状态、并请求数据
      this.setState({
        isScrollingDown: nextProps.currentRow > this.props.currentRow,
      });
      this.loadAsyncData()
    }
  }

  loadAsyncData() {/* ... */}
}

这段代码其实没有什么大问题,但存在更好的写法。当使用 getDerivedStateFromProps ,可以这样写:

class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    // 不再提供 prevProps 的获取方式
    if (nextProps.currentRow !== prevState.lastRow) {
      return {
        isScrollingDown: nextProps.currentRow > prevState.lastRow,
        lastRow: nextProps.currentRow,
      };
    }

    // 默认不改动 state
    return null;
  }
  
  componentDidUpdate() {
    // 仅在更新触发后请求数据
    this.loadAsyncData()
  }

  loadAsyncData() {/* ... */}
}

可以看到在这种情况下,开发者们将更“自发地”采取在 componentDidUpdate 编写触发异步请求的代码(因为别无选择:P),避免了一个问题——外部组件多次频繁更新传入多次不同的 props,而该组件将这些更新 batch 后仅仅触发单次自己的更新,如此一来前者的写法会导致不必要的异步请求,后者更节省资源。

-componentWillUpdate & +getSnapshotBeforeUpdate

componentWillUpdate 的常见用法是,在更新前记录 DOM 状态,结合更新后 componentDidUpdate 再次获取的 DOM 状态进行必要的处理。异步渲染的到来,使得 componentWillUpdate 的触发时机(它在异步渲染被取缔,但此处我们假想它仍然存在)与 componentDidUpdate 的触发时机间隔较大,因为异步渲染随时可能暂缓这一组件的更新。这样一来,之前的做法将变得不够稳定,因为这间隔久到 DOM 可能因为用户行为发生了变化。

为此,React 提供了 getSnapshotBeforeUpdate 。它的触发时机是 React 进行修改前(通常是更新 DOM)的“瞬间” ,这样一来在此获取到的 DOM 信息甚至比 componentWillUpdate 更加可靠。此外,它的返回值会作为第三个参数传入 componentDidUpdate ,这样做的好处很明显——开发者可以不必将为了协调渲染前后状态之用而产生的数据保存在组件实例上,用完即可销毁。

简单总结一下

React 自从迈上 16 的版本号,就像坐上了火箭,性能与 API 的进化令人瞩目。在不久的将来,异步渲染 将正式登场,带来渲染性能又一轮突破。本文所讨论的生命周期变化中,可以看到 React 团队为这一变化所做铺垫的良苦用心——旧 API 的移除,警醒或潜移默化地使开发者们遵循更加优秀的开发方式。从 v16.3 开始, 旧的 API 将逐渐被替代,而等到了 v17 将彻底废弃,看到这里的各位可以开始考虑给自己的项目升升级啦!

参考链接: https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html