概述
state是React组件中用来管理自身状态的的属性,state和props一起决定了组件最终渲染的结果,也就是UI=f(data)。props主要是用来在组件之间传递数据,而state主要管理组件自身的状态。接下来我们会由浅入深的介绍state,从如何使用state,state的同步异步更新,到使用状态管理工具来管理state。
state的初始化
在React class组件中构造函数是我们唯一能通过this.state 对state进行赋值的地方,当我们初始化一个class组件并设置state时:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
activeIndex: 1
}
}
}
而在函数式的组件中,在React hooks出现之前 函数式组件是不能处理state的木偶组件,在hooks推出之后,我们通常使用useState这个hook来初始化和更新state
function App(props) {
const {activeIndex, setActiveIndex} = useState(1);
}
state的改变
在React中,我们不能直接去改变state的值,这样是错误的
//Wrong
this.state.activeIndex = 2;
正确的state的修改方法是调用setState方法来修改state
//correct
this.setState({
activeIndex: 2
});
需要注意的是,像这样通过setState的方式修改state可能是异步的,React为了避免多次频繁setState可能出现的性能问题对setState操作进行了合并,进行组件的批量更新,所以我们不能马上依赖setState之后的state的结果。
this.setState({
activeIndex: 2
});
console.log(this.state.activeIndex) // 1
当然在实际的操作中肯定会有需要依赖更新后的state的应用场景,这里有两个方式来解决这个问题:1是传递一个函数到setState方法里,2是使用setState的第二个参数回调函数。
this.setState((state, props) => ({
activeIndex: 2
}))
this.setState({
activeIndex: 2
}, ()=> {
console.log(this.state.activeIndex);
})
React组件的更新过程
那么有小伙伴肯定会问了,React到底是怎么实现批量更新机制的呢。下面我们简单的依托React源码来介绍一下React的批量更新过程。所有的源码基于React 15.x
setState源码分析
首先我们来看下setState方法:
//ReactComponent.js
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback);
}
};
我们只截取最核心的几行代码来看,可以看出setState其实是将我们传入的新的partialState和组件的实例推入到了this.updater.enqueueSetState方法,我们继续看看enqueueSetState方法。
//ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
//将partial state存到_pendingStateQueue
queue.push(partialState);
//唤起enqueueUpdate
enqueueUpdate(internalInstance);
};
...
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
还是只看核心代码,这里其实就是将state存入了一个暂存队列,并调用了enqueueUpdate方法
//ReactUpdates.js
function enqueueUpdate(component) {
// 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState
//batchingStrategy:批量更新策略,通过事务的方式实现state的批量更新
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果batch已经开启,则将该组件保存在 dirtyComponents 中存储更新
dirtyComponents.push(component);
}
这段代码是是React批量更新的重点,可以看到React中存在一个batchingStrategy.isBatchingUpdates批量更新的标识,如果该标识为false,则直接更新,如果该标识为true,则会将该待更新组件实例推入到一个dirtyComponents待更新组件队列中等待批量更新。
到这里我们就可以发现React在它的更新中存在一个batchingStrategy的策略,其中的isBatchingUpdates属性标识了更新是否采取批量更新的策略。
那么现在还有两个关键的问题没有解答:1. isBatchingUpdates这个标识是什么时候置为true 2.dirtyComponent什么时候更新,以及后续更新的流程。
组件更新过程
下面先解答第二个问题,React的整个批量更新过程被包裹在一个叫ReactDefaultBatchingStrategy的事务之中。事务机制是React 的重要组成部分,事物(transaction) 主要包括三个部分:initialize, perform, close 理解起来也很简单,其实就相当于把perform执行函数包裹了起来,在之前会先执行initialize函数,在之后会执行close函数。
在事务ReactDefaultBatchingStrategy的close阶段执行了flushBatchedUpdates函数,flushBatchedUpdates执行完之后再将ReactDefaultBatchingStrategy.isBatchingUpdates重置为false,表示这次batch更新结束。
接下来的事情就很简单了flushBatchedUpdates函数循环遍历所有的dirtyComponent,对于每个组件调用ComponentWillReceiveProps和ComponentShouldUpdate生命周期判断是否要更新组件,确认更新则调用ComponentWillUpdate,然后调用render方法生成新的dom树进行diff算法,并将最小差异渲染到真实dom上,最后调用ComponentDidUpdate。
批量更新标识
这时候小伙伴们肯定又要问了,isBatchingUpdates这个标识是什么时候置为true的? 为什么setTimeout、async函数和原生事件当中setState不是异步的?
React在两种情况下会开启批量更新策略:1. 组件首次渲染时 2. 绑定React合成事件时。也就是说在ComponentDidMount 或其它生命周期中调用setState和给组件绑定onClick等合成事件时都会触发批量更新策略。
而serTimeout、async函数和原生事件则绕过了这两个规则,所以它的执行是同步的。然而在实际的开发中,我们应尽量使用React的批量更新策略,频繁的同步更新state可能带来性能问题。
使用状态管理工具管理状态
使用过React的小伙伴们可能或多或少都接触过状态管理工具,市面上比较著名的状态管理工具有flux、redux和mobx,由于redux出脱于flux并进行了一定的优化(主要是解耦了dispatch和action,并将reduce改为了纯函数)所以我们就主要介绍下redux和mobx。
redux的适用场景
说到redux的适用场景,redux的开发者是这样说的:"如果你不知道是否需要 Redux,那就是不需要它。"其实在大多数情况下,我们确实没有必要用到redux,很多情况下反而因为使用了redux导致项目的开发和维护变得更加复杂。如果你的项目有以下几个特点你可以考虑使用redux:
- 项目中用户有多个身份,且不同身份的用户有交互
- 与服务器有大量交互或一个view使用了多个数据源
- 组件的状态要被全局共享或组件需要改变全局状态
当然第3条完全可以通过context来实现,具体方式可以参考我的上篇文章。
redux的核心概念
1.store
store是一个状态管理容器,它通过createStore创建,createStore接收initialState和reducer两个参数。它暴露了4个api分别是:
- getState()
- dispatch(action)
- subscribe(listener)
- replaceReducer
前三个是比较常用的api,之后我们会来模拟实现一个createStore这个函数。
2.action
在redux思想中view的变化由action发出通知,action是一个包含type的简单对象,在redux思想中改变state的唯一方法就是触发action
3.dispatch
dispatch用来处理action,并传递给reducer,继而更新应用状态
4.reducer
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
在React中使用redux的数据流向如图所示:
createStore实现
下面我们来模拟实现一个createStore
function createStore(initialState, reducer) { let currentState = initialState; let listeners = []; getState() { return currentState; } dispatch(action) { currentState = reducer(action); listeners.forEach(fn => { fn(); }); } subscribe(listener) { listeners.push(listener); return unsunbscribe() { let idx = listeners.indexOf(listener); listeners.splice(idx, 1); } } return { getState, dispatch, subscribe };}
MobX的设计思想
Mobx则是提供了完全不同的思路来解决状态管理问题,他背后的哲学是:任何源自应用状态的东西都应该自动地获得。这句话怎么理解呢,这其实就是双向数据绑定的思想。其实现方式也和Vue中的双向数据绑定类似,通过defineProperty来劫持对象属性的get、set方法,当修改数据时,view也随之自动更新。
MobX的核心概念
1. Observable state(可观察的状态)
将变量设置为,可被观察的。具体的实现是通过 Object.defineProperty ,给被包装的属性套上 get 和 set 的钩子,在 get 中响应依赖收集,在 set 中触发监听函数。
那它是如何收集依赖和触发监听函数的呢,通过一个dependence-manager类管理了一个依赖的map,当一个被 observable 包装的属性值发生 set 行为的时候,就会触发 dependenceManager.trigger(obID); 从而触发遍历对应的监听函数列表,并且执行,这就是 autorun 的基本原理。
2. Computed values(计算值)
Computed 是一种特殊的类型,他即是观察者,也是被观察者,然后它最大的特性是,他的计算不是每次调用的时候发生的,而是在每次依赖的值发生改变的时候计算的,调用只是简单的返回了最后一次的计算结果。
3. Reactions
Reactions 和计算值很像,但它不是产生一个新的值,而是会产生一些副作用,比如打印到控制台、网络请求、递增地更新 React 组件树以修补DOM、等等。 简而言之,reactions 在 响应式编程和命令式编程之间建立沟通的桥梁。
总结
- React中通过state管理组件自身状态,只能在构造函数中通过
this.state
初始化状态,更新状态需调用setState方法。 - 由于React的批量更新机制,多次setState其实会被合并成一次,造成setState看上去是异步的,可以通过回调函数或者传入函数来获得最新的状态。
- React在组件首次渲染和合成事件中会开启批量更新策略事务,并在事务的close阶段批量更新组件。setTimeout、async函数和原生事件则不会触发批量更新策略。
- 可以通过Redux和MobX等状态管理工具辅助状态管理,redux使用store来管理应用状态,而MobX则是使用了双向数据绑定的思想