阅读 1445

React 状态管理:状态与生命周期

在《React 状态管理:从 Props 和 State 说起》一篇中,我们介绍了一个 React 组件状态管理的基础:props 和 state,以及这两个对象对 React 组件渲染的影响。在这一篇里,我们来看看这两个对象在组件的生命周期中,是如何与组件相互作用的。

生命周期

生命周期是一个很宽泛的概念。小到一个对象,大到一个应用,都有生命周期。而对于 UI 层面的开发来说,不管是 iOS 中 View Controller ,Android 中的 Activity/Fragment,还是 React/Vue 中的 UI 组件,也都有涉及到生命周期。这些对象的生命周期除了创建、销毁外,可能还涉及到一系列的操作,如加载、显示、更新、卸载等。

在 React 中,组件生命周期的维护一般是由框架负责的,包括组件的创建、销毁、变更等操作,我们一般不需要直接去手动去做处理。不过,框架同时也会提供一些 hook 方法,让我们在组件生命周期的某些特定时间点,可以加入我们自己的操作。

对于 React 组件来说,生命周期主要包含三个阶段:创建(挂载)过程、销毁(卸载)过程和存在期,每个阶段都有相应的 hook 方法,我们可以用一张图来概括:

我们在这不详细介绍每一个方法,而是主要介绍 props 和 state 两个对象是如何参与整个组件的生命周期的。

组件的生命周期相关可参考《深入React技术栈》

render() 方法

在这里需要重提一下 render() 方法。render() 方法是一个 class 组件必须实现的方法。props 和 state 对象中的值的改变会引起组件的重新渲染,从而会调用组件的 render() 方法。在这个方法中,我们可以从 props 和 state 中取出值来使用,以确定组件如何去渲染。

需要注意的是,render() 必须是一个纯函数,不能在这里面修改组件的状态或执行有副作用的操作。

Props 与生命周期

props 对象定义了组件的属性,我们在父组件中创建某个组件的实例时,可以通过属性向这个组件传递值。父组件通过属性传入的值,都会以 key-value 的形式存储在组件的 props 对象中。

声明 Props 的默认值

首先,我们可以为组件的属性设置默认值,这样父组件创建组件时,不需要设置所有的属性值。此时,组件内部使用 props 中的值时,如果父组件没有设置,则会使用默认值。

在 ES6 的类中,我们可以通过以下方式来设置默认值:

class Greeting extends React.Component {
  // ...
}

Greeting.defaultProps = {
  name: 'Mary'
};
复制代码

这里需要注意的是,defaultProps 是 Greeting 组件本身的属性(类属性),而不是组件实例的属性,这也就意味着在整个应用中,defaultProps 的值只会设置一次,而且是在实例化组件之前。让我们更直观地来看看这个流程:

class Component extends React.Component {
  render() {
    return React.createElement('span');
  }
}

Component.defaultProps = {fruit: 'persimmon'};

const container = document.createElement('div');
const instance = ReactDOM.render(
   React.createElement(Component, {fruit: 'mango'}),
   container,
);
复制代码

在通过 React.createElement() 创建组件实例之前,defaultProps 就已经设置好了。这就涉及到另一个问题,我们可以使用 static 语法糖在 class 内部来设置 defaultProps,此时我们不能在 defaultProps 中使用 this 去引用实例的属性(相信有面向对象语言基础的童鞋都知道问题所在)。

class Component extends React.Component {
  static defaultProps = {
  	 fruit: 'persimmon',
  	 // value: this.***,			// error
  }
  render() {
    return React.createElement('span');
  }
}
复制代码

修改 Props

修改 Props 对象中某个属性值,会引发组件的重新渲染。在这一过程中,会调用一系列的 hook 方法,如上图中所示。这个流程 state 的变更是差不多的。只是 props 的流程多了一个方法 componentWillReceiveProps()。实际上,在当前版本的 React 中,这个方法已被重命名为 UNSAFE_componentWillReceiveProps(nextProps),其参数 nextProps 包含修改后的新的属性值。该方法会在组件接收到新的属性值之前调用。

在这个方法中,我们可以对比修改前后的 props 的值,来做相应的处理,比如调用 setState() 方法来设置 state。不过官方显然不建议我们这样做,而在建议我们在 componentDidMount() 方法中来处理。

另外,如果父组件的行为导致组件重新渲染,也会触发这个方法执行,即使组件的属性值并没有改变。

componentWillReceiveProps() 之后的流程与 state 更改的流程一致,所以我们统一在下面来讨论这个过程。

State 与生命周期

state 是组件的内生状态。这让组件看上去像是一个状态机,在不同的状态下,显示不同的内容。

state 的初始化

state 是与每个组件实例自身相关的,我们通常在 constructor() 中,通过 this.state 来设置其初始值:

constructor(props) {
  super(props);
  // Don't call this.setState() here!
  this.state = { counter: 0 };
}
复制代码

这里有几点需要注意:

  1. 在 constructor() 构造器中必须首先调用 super(props),否则在构造器中 this.props 是未定义的,可能会引发 bug;
  2. constructor() 是唯一直接设置 this.state 的地方,其它地方都通过 setState() 方法来设置 state;
  3. 在 constructor() 中不能使用 setState() 方法来设置 state;
  4. 尽量避免将 props 的值拷贝到 state 中,一方面是没必要,另一方面是 props 修改时,对应 state 的值并不会同步修改(反模式);

修改 state

state 值的修改同样会引发组件的重新渲染,这个过程中调用的 hook 方法和 props 的变更一样。在这个过程中,会调用以下几个方法:

  1. shouldComponentUpdate(nextProps, nextState)
  2. componentWillUpdate(nextProps, nextState)
  3. render()
  4. componentDidUpdate(prevProps, prevState, snapshot)

shouldComponentUpdate(nextProps, nextState) 方法中,我们可以根据 nextProps 或 nextState 的值来确定是否需要重新渲染组件。默认是返回 true,如果返回 false,则后续流程会被中断。不过,这个方法的用途更多的是在于性能的优化,以减少不必要的渲染,而不是去依赖它来“阻止”渲染。

componentWillUpdate(nextProps, nextState) 是在即将更新组件之前的操作。这个方法在当前最新版本中同样被重命名为 UNSAFE_componentWillUpdate(nextProps, nextState)。在这个方法中,我们不能去调用 setState 方法,或者做其它可能会引起组件重新渲染的操作。

componentDidUpdate(prevProps, prevState, snapshot) 方法会在组件重新渲染完成之后调用。我们可以看到这时参数名已变成 prevPropsprevState,保存的是更新之前的 props 和 state。在这个方法中,可以去执行 DOM 操作,也可以去做网络请求等操作。同样,在这个方法中可以调用 setState() 方法,不过一定需要有一个条件语句来控制 setState() 方法的调用,否则会导致无限循环。

被遗弃的 hook 方法

实际上,在官方的最新文档中,有几个生命周期方法已被标记为 UNSAFE

  • UNSAFE_componentWillMount()
  • UNSAFE_componentWillUpdate()
  • UNSAFE_componentWillReceiveProps()

这几个方法对应的无前缀方法如下:

  • componentWillMount()
  • componentWillUpdate()
  • componentWillReceiveProps()

这几个方法在 v17 之前还会继续存在。不建议使用这几个方法是因为在这几个方法中做一些 side effect 时(如 setState() )时,会引发一些意想不到的问题。

在省去这几个方法后,生命周期的流程图就变成如下:

实际上在这个流程中我们可以看到下面这个方法:

static getDerivedStateFromProps(props, state)
复制代码

这是在 React 16.3 新加入的方法,主要是为了避免在 UNSAFE_componentWillReceiveProps 中使用 setState() 而产生的副作用。关于这个方法,需要注意几点:

  • 这个方法在每次 render() 之前都会调用;
  • 这是一个 static 方法,即类方法;
  • 它返回一个对象来更新 state,或者返回 null,什么也不做

小结

这里我们主要介绍了 props 和 state 与组件生命周期的关联。需要知道的就是这两个对象中的值发生改变时会引发组件的重新渲染,然后会执行一些 hook 方法。特别需要注意的是在这些 hook 方法中,我们需要正确的使用 setState() 方法,否则会导致一些未知的问题。

有了这些知识点,再加上组件事件处理相关的内容,就可以做一些最基本的状态管理了。不过,如果直接在组件中管理所有状态,可能会造成一些问题:

  • 组件需要处理很多事情,如网络请求、本地缓存等,这样组件的代码会很多很乱,这是我们不愿意看到的;
  • 组件中产生副作用的操作过多,不利于组件的复用;

为此,我们需要想一些办法来解决这些问题。这就需要引入一起框架,如 Flux,Redux 等。下一篇,我们先来讲讲 Flux。

参考

  1. 官方文档:React.Component

知识小集是一个团队公众号,主要定位在移动开发领域,分享移动开发技术,包括 iOS、Android、小程序、移动前端、React Native、weex 等。每周都会有 原创 文章分享,我们的文章都会在公众号首发。欢迎关注查看更多内容。

关注下面的标签,发现更多相似文章
评论