手把手教你React(四)-状态管理

1,097 阅读9分钟

概述

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:

  1. 项目中用户有多个身份,且不同身份的用户有交互
  2. 与服务器有大量交互或一个view使用了多个数据源
  3. 组件的状态要被全局共享或组件需要改变全局状态

当然第3条完全可以通过context来实现,具体方式可以参考我的上篇文章

redux的核心概念

1.store

store是一个状态管理容器,它通过createStore创建,createStore接收initialState和reducer两个参数。它暴露了4个api分别是:

  1. getState()
  2. dispatch(action)
  3. subscribe(listener)
  4. 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 在 响应式编程和命令式编程之间建立沟通的桥梁。

总结

  1. React中通过state管理组件自身状态,只能在构造函数中通过this.state初始化状态,更新状态需调用setState方法。
  2. 由于React的批量更新机制,多次setState其实会被合并成一次,造成setState看上去是异步的,可以通过回调函数或者传入函数来获得最新的状态。
  3. React在组件首次渲染和合成事件中会开启批量更新策略事务,并在事务的close阶段批量更新组件。setTimeout、async函数和原生事件则不会触发批量更新策略。
  4. 可以通过Redux和MobX等状态管理工具辅助状态管理,redux使用store来管理应用状态,而MobX则是使用了双向数据绑定的思想