React 学习笔记:如何替换过时的生命周期

825 阅读3分钟

本文是异步渲染之更新博文的总结整理。

在 React 16 使用 Fiber 来对 Virtual DOM 可以进行增量式渲染。由于 Fiber 能使渲染阶段暂停,中止和重新启动。所以一些生命周期可能会被调用多次。如果这些生命周期错误的被使用,可能导致 bug,这篇文章介绍如何替换它们。

如果你对 React 的生命周期还不熟悉,先阅读# React 学习笔记:React 生命周期

替换过时的生命周期

在异步渲染(Fiber)时,有 3 个过时的声明周期函数,如下所示:

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

这些声明周期经常被滥用,在异步渲染时还可能会导致 bug。所以需要替换它们。本文主要介绍替换的方法。

生命周期函数 getDerivedStateFromProps

新的生命周期函数 getDerivedStateFromProps

class Example extends React.Component {
  static getDerivedStateFromProps(props, state) {
    // ...
  }
}

它在组件实例化后或重新渲染之前调用。返回对象更新 state, 或者返回 null 不更新 state

替换 componentWillMount

场景 1 初始化 state

以前写法, 在 componentWillMount 中初始化 state

替换写法, 在 constructor 中初始化 state

场景 2 异步获取数据

以前写法,在 componentWillMount 中异步获取数据

替换写法,在 componentDidMount 中异步获取数据

场景 3 添加订阅

以前写法,在 componentWillMount 中异步添加订阅

替换写法,在 componentDidMount 中异步添加订阅

替换 componentWillReceiveProps

场景 1 比较 props 更新 state

componentWillReceiveProps 写法

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

  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      this.setState({
        isScrollingDown:
          nextProps.currentRow > this.props.currentRow,
      });
    }
  }
}

替换为 getDerivedStateFromProps 后的写法

// After
class ExampleComponent extends React.Component {
  // 在构造函数中初始化 state,
  // 或者使用属性初始化器。
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(props, state) {
    if (props.currentRow !== state.lastRow) {
      return {
        isScrollingDown: props.currentRow > state.lastRow,
        lastRow: props.currentRow,
      };
    }

    // 返回 null 表示无需更新 state。
    return null;
  }
}

场景 2 比较 props 更新副作用

componentWillReceiveProps 写法

// Before
class ExampleComponent extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (this.props.isVisible !== nextProps.isVisible) {
      logVisibleChange(nextProps.isVisible);
    }
  }
}

替换为 componentDidUpdate 后的写法

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (this.props.isVisible !== prevProps.isVisible) {
      logVisibleChange(this.props.isVisible);
    }
  }
}

场景 3 比较 props 获取外部数据

componentWillReceiveProps 写法

// Before
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.id !== this.props.id) {
      this.setState({externalData: null});
      this._loadAsyncData(nextProps.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // 渲染加载状态 ...
    } else {
      // 渲染真实 UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = loadMyAsyncData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

替换为 componentDidUpdate + getDerivedStateFromPorps 后的写法

// After
class ExampleComponent extends React.Component {
  state = {
    externalData: null,
  };

  static getDerivedStateFromProps(props, state) {
    // 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。
    // 清除之前加载的数据(这样我们就不会渲染旧的内容)。
    if (props.id !== state.prevId) {
      return {
        externalData: null,
        prevId: props.id,
      };
    }
    // 无需更新 state
    return null;
  }

  componentDidMount() {
    this._loadAsyncData(this.props.id);
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.externalData === null) {
      this._loadAsyncData(this.props.id);
    }
  }

  componentWillUnmount() {
    if (this._asyncRequest) {
      this._asyncRequest.cancel();
    }
  }

  render() {
    if (this.state.externalData === null) {
      // 渲染加载状态 ...
    } else {
      // 渲染真实 UI ...
    }
  }

  _loadAsyncData(id) {
    this._asyncRequest = loadMyAsyncData(id).then(
      externalData => {
        this._asyncRequest = null;
        this.setState({externalData});
      }
    );
  }
}

替换 componentWillUpdate

场景 1 调用外部回调

componentWillUpdate 写法

// Before
class ExampleComponent extends React.Component {
  componentWillUpdate(nextProps, nextState) {
    if (
      this.state.someStatefulValue !==
      nextState.someStatefulValue
    ) {
      nextProps.onChange(nextState.someStatefulValue);
    }
  }
}

替换为 componentDidUpdate 后的写法

// After
class ExampleComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (
      this.state.someStatefulValue !==
      prevState.someStatefulValue
    ) {
      this.props.onChange(this.state.someStatefulValue);
    }
  }
}

场景 2 组件更新前读取 DOM

componentWillUpdate 写法

class ScrollingList extends React.Component {
  listRef = null;
  previousScrollOffset = null;

  componentWillUpdate(nextProps, nextState) {
    // 我们正在向列表中添加新项吗?
    // 捕获滚动位置,以便我们稍后可以调整滚动位置。
    if (this.props.list.length < nextProps.list.length) {
      this.previousScrollOffset =
        this.listRef.scrollHeight - this.listRef.scrollTop;
    }
  }

  componentDidUpdate(prevProps, prevState) {
    // 如果我们刚刚添加了新项,并且设置了 previousScrollOffset。
    // 调整滚动位置,以便这些新项不会把旧项挤出视图。
    if (this.previousScrollOffset !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight -
        this.previousScrollOffset;
      this.previousScrollOffset = null;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}>{/* ...内容... */}</div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

替换为 getSnapshotBeforeUpdate 后的写法

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 我们正在向列表中添加新项吗?
    // 捕获滚动位置,以便我们稍后可以调整滚动位置。
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果我们刚刚添加了新项,并且有了快照值。
    // 调整滚动位置,以便这些新项不会把旧项挤出视图。
    // (此处的快照是从 getSnapshotBeforeUpdate 返回的值)
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}>{/* ...内容... */}</div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}

总结

componentWillMount 主要是初始化 state,获取外部数据,订阅。 初始化 state 由 constrctor 替换;获取外部数据,订阅由 componentDidMount 替换

conponentWillReceiveProps 的两个主要作用 ** 更新副作用 ** 和 ** state **。 更新副作用由 componentDidUpdate 替换, state 由 getDerivedStateFromProps 替换.
getDerivedStateFromProps 通过把 props 存在 state,对比 props 和 存的state(旧 props), 实现 conponentWillReceiveProps 对比 props 和 nextProps 的特性

componentWillUpdate 主要是组件更新前,调用外部回调和获取 DOM 更新前调用外部回调可以用 componentDidUpdate 替换;获取更新前 DOM 用 getSnapshotBeforeUpdate 替换。

参考文章

1,异步渲染之更新