深入理解State和setState()

本文主要探讨React的State的设计哲学,以及总结一下setState的用法。

一种对State的定义:

State可被视为一个React组件中的一个集合,这个集合的内容是该组件UI中可变状态的数据

所谓可变状态的数据,就是在当前组件中可以被修改(或被更新)的数据。

如果我们要在一个组件中定义一个变量,如何确定适合不适合让这个变量作为一个State呢?

组件对State的要求:

  • State必须是能代表一个组件UI呈现的完整状态集:组件UI的任何改变,都可以从State的变化中反映出来。
  • State必须是代表一个组件UI呈现的最小状态集:State中的所有状态都是用于反映组件UI的变化,没有任何多余的状态,也不需要通过其他状态计算而来的中间状态。

可见,第一个要求为“完整”,第二个要求为“最小”。

变量能否作为State的判据:

组件中用到的一个变量应不应该作为一个组件的State,可以通过下面的4条依据进行判断:

1.这个变量是否是通过Props从父组件中获取?如果是,那么它不适合以State来表示。

2.这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不适合以State来表示。

3.这个变量是否可以通过其他状态(State)或者属性(Props)计算得到?如果是,那么它不适合以State来表示。

4.这个变量是否在render方法中作为一个用于渲染的数据?如果不是,那么它不适合以State来表示。这种情况下,这个变量更适合定义为组件的一个普通属性,例如在组件中用到的定时器,就应该直接定义为this.timer,而不是this.state.timer

5.另外要考虑这个状态需不需要状态提升到父组件中。

State与Props的区别:

State在当前组件中是可变的,满足组件UI变化的需求;

Props对于子组件来说是只读的。

如何正确修改State?

不要直接给state赋值

// 错误,这样组件不可能会重新渲染,结果会不符合我们的目的:
this.state.comment = 'Hello';

只有在组件的构造函数中初始化State的时候才允许这样直接赋值;其他绝大多数时候,应该使用setState() ,在本文的最后,我们会详细介绍setState()的用法:

// 正确:
this.setState({comment: 'Hello'});

State的更新可能是异步的

React可以将多个setState()调用合并成一个调用来提高性能。同时,Props的更新机制也是同理。这就是“异步更新”。

因为this.propsthis.state可能是异步更新的,你不应该依靠它们的值来计算下一个状态。

弥补这个缺陷:

我们不能直接通过this.statethis.props获得State和Props的最新状态,但是在this.setState的时候,State和Props的最新状态可以通过一个回调函数来获得:

// 正确:
this.setState((preState, props) => ({
  counter: preState.quantity + 1 + props.xxxx; 
}))

上述回调函数的第一个参数preState可捕获到最新的上一个State;第二个参数props可捕获到最新的Props

State的更新是一个浅合并的过程

当调用setState修改组件状态时,只需要传入发生改变的State,而不必组件完整的State,因为组件State的更新是一个浅合并(Shallow Merge)的过程。例如,一个组件初始化时的状态为:

this.state = {
  title : 'React',
  content : 'React is an wonderful JS library!'
}

如果你只需要修改title,你应该:

this.setState({title: 'Reactjs'});

React会合并新的title到原来的组件状态中,同时保留原有的状态content,合并后的State结果为:

{
  title : 'Reactjs',
  content : 'React is an wonderful JS library!'
}

State与Immutable

React官方建议把State当作是不可变对象,也就是说,当你有修改this.state的值的冲动的时候,你应该做的是:重新创建一个新值来赋给this.state

按照这种思想具体怎么操作呢?我们再来细分一下:

通常情况下的赋值:

适用的类型有:数字,字符串,布尔值,null, undefined。见例子即可:

this.setState({
  count: 1,
  title: 'Redux',
  success: true
})

状态的值的类型是数组

如有一个数组类型的状态books,当向books中增加一本书时,使用数组的concat方法或ES6的数组扩展语法(spread syntax):

// 方法一:将state先赋值给另外的变量,然后使用concat创建新数组
var books = this.state.books; 
this.setState({
  books: books.concat(['React Guide']);
})

// 方法二:使用preState、concat创建新数组
this.setState(preState => ({
  books: preState.books.concat(['React Guide']);
}))

// 方法三:ES6数组扩展 spread syntax
this.setState(preState => ({
  books: [...preState.books, 'React Guide'];
}))

当从books中截取部分元素作为新状态时,使用数组的slice方法:(利用splice返回的数组也是同理)

// 方法一:将state先赋值给另外的变量,然后使用slice创建新数组
var books = this.state.books; 
this.setState({
  books: books.slice(1,3);
})

// 方法二:使用preState、slice创建新数组
this.setState(preState => ({
  books: preState.books.slice(1,3);
}))

当从books中过滤部分元素后,作为新状态时,使用数组的filter方法:

// 方法一:将state先赋值给另外的变量,然后使用filter创建新数组
var books = this.state.books; 
this.setState({
  books: books.filter(item => {
    return item != 'React'; 
  });
})

// 方法二:使用preState、filter创建新数组
this.setState(preState => ({
  books: preState.books.filter(item => {
    return item != 'React'; 
  });
}))

注意不要使用push、pop、shift、unshift等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat、slice、splice、filter能返回一个新的数组。

状态的值的类型是普通对象(不包含字符串、数组)

使用ES6 的Object.assgin方法

// 方法一:将state先赋值给另外的变量,然后使用Object.assign创建新对象
var owner = this.state.owner;
this.setState({
  owner: Object.assign({}, owner, {name: 'Jason'});
})

// 方法二:使用preState、Object.assign创建新对象
this.setState(preState => ({
  owner: Object.assign({}, preState.owner, {name: 'Jason'});
}))

使用对象扩展语法(object spread properties):

// 方法一:将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
var owner = this.state.owner;
this.setState({
  owner: {...owner, name: 'Jason'};
})

// 方法二:使用preState、对象扩展语法创建新对象
this.setState(preState => ({
  owner: {...preState.owner, name: 'Jason'};
}))

为什么推荐组件的状态是不可变对象呢?

一方面是因为不可变对象方便管理和调试,了解更多可参考这里;另一方面是出于性能考虑,当对象组件状态都是不可变对象时,我们在组件的shouldComponentUpdate方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的render调用。当我们使用React 提供的PureComponent时,更是要保证组件状态是不可变对象,否则在组件的shouldComponentUpdate方法中,状态比较就可能出现错误,因为PureComponent执行的是浅比较(比较对象的引用)。

setState的各种写法:

在本文上一章《如何正确修改State》中,我们了解了各种规则,顺带对setState有了初步的认识。接下来我们深挖一下setState的各种写法,以及它们之间的区别。

上文我们说过,State的更新可能是异步的

setState将组件中的state变化塞入一个队列里面,并且告诉该组件及其子组件需要用更新后的状态来重新渲染。这是在event handlers函数中或者是server responses函数中更新UI的最常用的方式。

我们应当将setState视为一次请求而不是一次立即执行更新组件的命令。因为为了更好的性能,React可能会收集几次setState的请求,然后再一次性更新State。又或者React可能会推迟更新State。

所以如果调用setState之后,再使用this.state.xxx获取State的值, React不会保证能立刻拿到更新后的结果。

如果你的一些脚本一定需要在State更新之后再执行,你可以将代码写在componentDidUpdate里面,或者利用setState的回调(setState(updater, callback))。

顺便关心一下setState与页面重新渲染之间的关系:

我的总结如下:

  • setState总是会触发页面的重新渲染。
  • 在调用setState时,如果在shouldComponentUpdate返回false,则不会触发重新渲染。
  • 在调用setState时,更新可变对象的状态(可变对象mutable objects与不可变对象immutable objects是相对的),也不会触发重新渲染。

它们的原则是:只有当新状态不同于之前状态时才会触发重新渲染,这是为了减少不必要的渲染。

setState的两种写法:

setState的写法可以分为两类:

  1. setState(updater[, callback]):第一个参数是一个updater函数;第二个参数是个回调函数(可选)
  2. setState(stateChange[, callback]):第一个参数是一个对象;第二个参数同上(可选)

第二个参数都一样,是一个可选的回调函数,其将会在setState执行完成同时组件被重渲之后再执行。通常,对于这类逻辑,我们推荐使用componentDidUpdate来处理。

第一个参数是updater函数;第二个参数回调函数比较少用,省略

例子如下:

this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

如上文所说,preState和props都能拿到最新的数据,不再多讲。updater函数的返回结果通常是一个对象!

第一个参数是对象;第二个参数回调函数比较少用,省略

例子如下:

this.setState({quantity: 2})
文章目录
  1. 1. 一种对State的定义:
  2. 2. 组件对State的要求:
  3. 3. 变量能否作为State的判据:
  4. 4. State与Props的区别:
  5. 5. 如何正确修改State?
    1. 5.1. 不要直接给state赋值
    2. 5.2. State的更新可能是异步的
      1. 5.2.1. 弥补这个缺陷:
    3. 5.3. State的更新是一个浅合并的过程
    4. 5.4. State与Immutable
      1. 5.4.1. 通常情况下的赋值:
      2. 5.4.2. 状态的值的类型是数组
      3. 5.4.3. 状态的值的类型是普通对象(不包含字符串、数组)
      4. 5.4.4. 为什么推荐组件的状态是不可变对象呢?
  6. 6. setState的各种写法:
    1. 6.1. 顺便关心一下setState与页面重新渲染之间的关系:
    2. 6.2. setState的两种写法:
      1. 6.2.1. 第一个参数是updater函数;第二个参数回调函数比较少用,省略
      2. 6.2.2. 第一个参数是对象;第二个参数回调函数比较少用,省略
|