本篇目录:

  • state vs 普通属性
  • state vs props
  • state的判断依据
  • state的修改
  • state与不可变对象

组件state必须能代表一个组件UI呈现的完整状态集,即组件的任何UI改变都可以从state的变化中反映出来;同时,state还必须代表一个组件UI呈现的最小状态集,即state中的所有状态都用于反映组件UI的变化,没有任何多余的状态,也不应该存在通过其他状态计算而来的中间状态。

state vs 普通属性

首先,什么是普通属性?

我们的组件都是使用ES6class定义的,所以组件的属性其实也就是class的属性(更确切的说法是class实例化对象的属性,但因为JavaScript本质上是没有类的定义的,class只不过是ES6提供的语法糖,所以这里模糊化类和对象的区别)。

ES6中,可以使用this.{属性名}定义一个class的属性,也可以说属性是直接挂载到this下的变量。因此,stateprops实际上也是组件的属性,只不过它们是React为我们在Component class中预定义好的属性。除了stateprops以外的其他组件属性称为组件的普通属性。

比如,组件中需要一个定时器来自动更新显示时间时,我们都会需要一个timerId来保存该定时器,已在需要的能够清除它。这就是一个普通的属性,因为它和组件要渲染的内容没有直接关系。

因此,当我们在组件中需要用到一个变量,并且它与组件的渲染无关时,就应该把这个变量定义为组件的普通属性,直接挂载到this下,而不是作为组件的state。或者更直观的判断是,看组件render方法中是否使用到了这个变量,如果没有,它就是一个普通属性

state vs props

stateprops又有什么区别呢?

stateprops都直接和组件的UI渲染有关,它们的变化都会触发组件重新渲染,但props对于使用它的组件来说是只读的,是通过父组件传递过来的,要想修改props,只能在父组件中修改;而state是组件内部自己维护的状态,是可变的。

其实区分stateprops的关键就是,‘控制权’是在组件自身,还是由其父组件来控制的。

state的判断依据

(1)这个变量是否通过props从父组件中获取?如果是,那么它不是一个状态。

(2)这个变量是否在组件的整个生命周期中都保持不变?如果是,那么它不是一个状态。

(3)这个变量是否可以通过其他状态(state)或者属性(props)计算得到?如果是,那么它不是一个状态。

(4)这个变量是否在组件的render方法中使用?如果不是,那么它不是一个状态。这种情况下,这个变量更适合定义为组件的一个普通属性。

state的修改

1、state不能直接进行修改

直接赋值形式修改state,不会触发组件的render。修改state需要通过setState方法进行修改。

1
2
3
4
5
6
7
// 错误
this.state.title = 'React';

// 正确
this.setState({
title: 'React'
})

2、state的更新是异步的

调用setState时,组件的state并不会立即改变,setState只是把要修改的状态放入一个队列中,React会优化真正的执行时机,并且出于性能原因,可能会将多次setState的状态修改合并成一次状态修改。

所以不要依赖当前的state,计算下一个state。当真正执行状态修改时,依赖的this.state并不能保证是最新的state,因为React会把多次state的修改合并成一次,这时this.state还是这几次state修改前的state

另外,需要注意的是,同样不能依赖当前的props计算下一个状态,因为props的更新也是异步的。

如,电商类购物车添加购买数量时,如果连续点击两次,就会连续调用两次this.setState({count: this.state.count + 1}),在React合并多次修改为一次的情况下,相当于等价执行了下面的操作:

1
2
3
4
5
Object.assign(
previousState,
{count: this.state.count + 1},
{count: this.state.count + 1}
)

于是,最终只会增加一次操作的值。

如果有这样的需求,可以使用另一个接收一个函数作为参数的setState,这个函数有两个参数,第一个是当前最新状态(本次组件状态修改生效后的状态)的前一个状态preState(本次组件状态修改前的状态),第二个参数是当前最新的属性props。代码如下:

1
2
3
this.setState((preState, props) => ({
count: preState.count + 1;
}))

3、state更新是一个合并的过程

我们在每次调用setState修改state时,并不是需要把所有的值都进行修改,而只是设置我们要修改的部分值。React会合并新值导员的组件state中,同时保留没有发生变化的原来的state的值。

state与不可变对象

React官方建议把state当作不可变对象,一方面,直接修改this.state,组件并不会重新render;另一方面,state中包含的所有状态都应该是不可变对象。当state中的某个状态发生变化时,应该重新创建这个状态对象,而不是直接修改原来的状态。

可以分为下面三种情况:

1.状态的类型是不可变类型(数字、字符串、布尔值、null、undefined)

1
2
3
4
5
this.setState({
count: 1, // 数字类型
title: 'React', // 字符串类型
success: true // 布尔类型
})

2.状态的类型是数组

1
2
3
4
5
6
7
8
9
// 方法一:使用preState、concat创建新数组
this.setState(preState => ({
books: preState.books.concat(['React']);
}))

// 方法二:ES6 spread syntax
this.setState(preState => ({
books: [...preState.books, 'React'];
}))

当从数组中截取部分元素作为新的状态时,可使用数组的slice方法:

1
2
3
this.setState(preState => ({
books: preState.books.slice(1,3);
}))

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

1
2
3
4
5
this.setState(preState => ({
books: preState.books.filter(item => {
return item !== 'React';
});
}))

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

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

(1)使用ES6Object.assgin方法:

1
2
3
this.setState(preState => ({
owner: Object.assign({}, preState.owner, {name: 'Tom'});
}))

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

1
2
3
this.setState(preState => ({
owner: {...preState.owner, name: 'Tom'};
}))

总结一下,创建新的状态对象的关键是,避免使用会直接修改原对象的方法,而是使用可以返回一个新对象的方法。当然,也可以使用一些Immutable的JS库(如Immutable.js)实现类似的效果。

为什么React推荐组件的状态是不可变对象呢?一方面是因为对不可变对象的修改会返回一个新对象,不需要担心原有对象在不小心的情况下被修改导致的错误,方便程序的管理和调试;另一方面是出于性能考虑,当对象组件状态都是不可变对象时,在组件的shouldComponentUpdate方法中仅需要比较前后两次状态对象的引用就可以判断状态是否真的改变,从而避免不必要的render调用。