状态管理是什么?

在组件化开发的新前端发展史上,组件的发展提供了更好的编码效率,更好的代码阅读性,维护性,补充HTML5语义化标签的不足。前端承担了越来越多的任务,特别是做一个spa项目。然而我们父子组件沟通可以通过props和回调,但是在两个组件我们并不知道它们的调用关系的时候,如何进行沟通呢?

所以:引入了状态管理的概念。比如在react中,通信解决方式有状态提升和发布、订阅,状态提升又分为container组件定义和使用context属性传递:

  1. container组件是说把两个组件需要共享的状态提升到一个共同的根组件上,通过 props 传递 state 以及 changeState 的方法。
  2. context属性传递是说在使用React.createContext()声明了context对象,这个Context对象包含两个组件,。使用这个对象的provider包装需要共用这个context的父组件,把要传递的值通过value属性传递给子组件;使用这个对象的consumer包装要使用context属性的子组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const ThemeContext = React.createContext({
background: 'red',
color: 'white'
});

class App extends React.Component {
render () {
return (
<ThemeContext.Provider value={{background: 'green', color: 'white'}}>
<Header />
</ThemeContext.Provider>
);
}
}

class Header extends React.Component {
render () {
return (
<Title>Hello React Context API</Title>
);
}
}

class Title extends React.Component {
render () {
return (
<ThemeContext.Consumer>
{context => (
<h1 style={{background: context.background, color: context.color}}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
);
}
}

// 也可以不使用consumer
class Title extends React.Component {
static contextType = ThemeContext;
render () {
return (
<h1 style={{background: this.context.background, color: this.context.color}}>
{this.props.children}
</h1>
);
}
}

它会使组件更新更加困难。这个provider有更新,它的子代所有组件会重新渲染。不受shouldComponentUpdate方法约束,更改是通过使用与Object.is相同的算法比较新值和旧值来确定的。
旧版本的Context的更新需要依赖setState(),是不可靠的,不过这个问题在新版的API中得以解决。
但如果开发组件过程中可以确保组件的内聚性,可控可维护,不破坏组件树的依赖关系,影响范围小,可以考虑使用Context解决一些问题。

context运用于react-router: https://www.jianshu.com/p/eba2b76b290b
如果组件的功能不能单靠组件自身来完成,还需要依赖额外的子组件,那么可以利用Context构建一个由多个子组件组合的组件。为了让相关的子组件一同发挥作用,react-router的实现方案是利用Context在<Router /><Link />以及<Route />这些相关的组件之间共享一个router,进而完成路由的统一操作和管理。
<Router />的核心就是为子组件提供一个带有router属性的Context,同时监听history,一旦history发生变化,便通过setState()触发组件重新渲染。
<Link />的核心就是渲染<a>标签,拦截<a>标签的点击事件,然后通过<Router />共享的router对history进行路由操作,进而通知<Router />重新渲染。
<Route />有一部分源码与<Router />相似,可以实现路由的嵌套,但其核心是通过Context共享的router,判断是否匹配当前路由的路径,然后渲染组件。

  1. 发布订阅就是一个组件订阅,一个组件发布。

因此,出现了独立管理状态的地方。可以实现数据的订阅、发布。如flux/redux/vuex/mobx。他们有什么区别呢?

一句话总结:
它们都是基于单向数据流的状态管理方法论。Flux最早提出,作为对传统前端MVC的一种改进(我不认为是颠覆)。Redux深受Flux的启发,又加入了函数式编程的思想,算是Flux的极大增强版本。Vuex可以说是基于Flux并且吸收了Redux的一些特点,但它与Vue是紧密捆绑的。Redux其实除了在React中广泛应用。

flux

View: 确定相应的Store以及监听其变化来更新视图。发起Action。
Action:每个Action都是一个对象,包含一个actionType属性(说明动作的类型)和一些其他属性(用来传递数据)
Dispatcher:全局唯一。逻辑简单,只用来派发action去相应的store。通过 AppDispatcher.register() 来登记各种Action的回调函数。
Store: 存放view中的数据。发送change事件,通过view中定义的handler捕捉变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//store
var EventEmitter = require('events').EventEmitter;
var ListStore = assign({}, EventEmitter.prototype, {
// data in View
items: [],
emitChange: function () {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
}
});

// view
ListStore.addChangeListener(this._onChange); //如果listStorage值变化change,调用回调this._onChange

var ButtonActions = {
addNewItem: function (text) {
AppDispatcher.dispatch({
actionType: 'ADD_NEW_ITEM',
text: text
});
},
};

ButtonActions.addNewItem('new item');

//这里是dispatcher
// dispatcher/AppDispatcher.js 全局唯一
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();

//注册action回调函数
AppDispatcher.register(function (action) {
switch(action.actionType) {
case 'ADD_NEW_ITEM':
ListStore.addNewItemHandler(action.text);
ListStore.emitChange(); //通知listStorage触发change, 调用注册的回调
break;
default:
// no op
}
})

可以看出,flux更新逻辑在store。dispatcher只是将action进行处理,然后调用store里面的方法去更新数据。

redux

Redux = Reducer + Flux

  1. Redux将Flux中的Dispatcher并入了Store。也可以理解为Redux没Dispatcher。Redux的设想是用户永远不会变动数据,应该在reducer中返回新的对象来作为应用的新状态。
  2. Redux增加了Reducer.
    注:通过代码对比的直观感受就是Flux中的view需要知道具体对应哪个store。而在Redux中,store成为一个被所有view共享的公共对象,view只需要通过store.dispatch()来发送action,无需关心具体处理函数。

View: 通过全局唯一的store dispatch action 以及获取最新state
Action: 与flux一致。
reducer: 是current state 和 action 为参数计算new state的纯函数。
Store: 全局唯一。Dispatcher功能已被整合进store:store.dispatch()。state 一旦有变化,store 就会调用通过store.subscribe()注册的回调函数(一般是render)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const createStore = (reducer) => {
let state; // 定义存储的state
const listeners = [];

// getState的作用很简单就是返回当前是state
const getState = () => state;

//定义一个派发函数
//当在外界调用此函数的时候,会修改状态
const dispatch = (action) => {
//调用reducer函数修改状态,返回一新的状态并赋值给这个局部状态变量
state = reducer(state, action);
//依次调用监听函数,通知所有的监听函数
listeners.forEach(listener => listener());
}

//订阅此状态的函数,当状态发生变化的时候记得调用此监听函数
const subscribe = function(listener) {
//先把此监听 加到数组中
listeners.push(listener);

//返回一个函数,当调用它的时候将此监听函数从监听数组移除
return function() {
listeners = listeners.filter(l => l !== listener);
}
}

//默认调用一次dispatch给state赋一个初始值
dispatch();

return {
getState,
dispatch,
subscribe
}
}

// 订阅状态变化事件,当状态变化时用监听函数
store.subscribe(render);
store.dispatch({
type: 'add',
payload: '3'
});

createStore(reducer);

[三大原则,store 唯一,state 只读, reducer 纯函数。]

所以实现我们大部分使用:redux + react-redux + redux-chunk + redux-immutable 。用来与react框架进行交互。redux的dispatch默认只能传输action, 引用redux-thunk中间件,可以让action创建函数先不返回一个action对象,而是返回一个函数,函数传递两个参数(dispatch,getState),在函数体内进行业务逻辑的封装。通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mapStateToProps = state => {
return {
todos: state.get('todos')
}
}

const mapDispatchToProps = dispatch => {
return {
addTodo: info => {
dispatch(addTodo(info))
}
}
}

export default connect(mapStateToProps,mapDispatchToProps)(TodoList);

//使用指定的 React Redux 组件 <Provider> 来 魔法般的 让所有容器组件都可以访问 store,而不必显示地传递它。
<Provider store={store}>
<App />
</Provider>

React Redux 组件 来魔法般的让所有容器组件都可以访问 store,而不必显示地传递它。

容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。
你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。

connect 出来的 HOC, 通过 Provider 提供的 context 上的 store,在内部向 store subscribe 了 onStateChange 事件。只要派发了 action,就会触发一次 onStateChange 事件,HOC 就能感知 store 的更新再根据 onStateChange 的结果决定是否要 update。

vuex

只用来读取的状态集中放在store中;改变状态的方式是提交mutations,这是个同步的事物;异步逻辑应该封装在action中。

state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。mutation 必须是同步函数。
getters
类似vue的计算属性,主要用来过滤一些数据。
action
actions可以理解为通过将mutations里面处理数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const store = new Vuex.Store(storeSettings)

new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

//这个storeSettings
const storeSettings = {
state: {
selfInfo: {},
},
getters: {
//会根据依赖缓存起来
getType: state = > {
return state.type
}
},
mutations: {
updateSelfInfo(state, payload) {
state.selfInfo = payload;
},
},
actions: {
Login_Action({commit}, userInfo) {
return new Promise((resolve, reject) => {
(async ()=>{
try{
const data = await getMyBriefInfo();
commit('updateSelfInfo', data);
resolve();
} catch (e) {
let e_j = (e && JSON.parse(e) && JSON.parse(e).message) ? JSON.parse(e).message : '登录失败,请稍后重试!';
commit('clearToken');
reject(e_j);
}
})();
})
},
}
}

//组件中使用
this.$store.dispatch('Login_Action', {
userName, password, checked, code
}).then(() => {

}, (info) => {

});

this.$store.state.selfInfo //获取state
this.$store.getters.getType //获取getters
this.$store.commit('updateSelfInfo', data)

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块

局部模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},

getters: {
//根节点状态会作为第三个参数暴露出来
doubleCount (state, getters, rootState) {
return state.count * 2
}
},

actions: {
//局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> moduleA 的状态

this.$store.dispatch('a/incrementIfOddOnRootSum')

使用mutation来替换redux中的reducer
Vuex有自动渲染的功能,所以无需要专门监听state。
Vuex中的action是一个函数集合对象,用于async/sync commit mutaions. 和Redux或者Flux中的action只是简单对象有本质不同,只是叫了一个相同名字。

mobx

通过observable观察某一个变量,当该变量产生变化时,对应的autorun内的回调函数就会发生变化。 Observable 、Computed 依赖state产生 obserable 、Autonrun、Action、Observer,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {observable, autorun} from 'mobx';

var todoStore = observable({
/* some observable state */
todos: [],

/* a derived value */
get completedCount() {
return this.todos.filter(todo => todo.completed).length;
}
});

/* a function that observes the state */
autorun(function() {
console.log("Completed %d of %d items",
todoStore.completedCount,
todoStore.todos.length
);
});

/* ..and some actions that modify the state */
todoStore.todos[0] = {
title: "Take a walk",
completed: false
};
// -> synchronously prints: 'Completed 0 of 1 items'

todoStore.todos[0].completed = true;

Action:定义改变状态的动作函数,包括如何变更状态;
Store:集中管理模块状态(State)和动作(action);
Derivation(衍生):从应用状态中派生而出,且没有任何其他影响的数据,我们称为derivation(衍生),衍生在以下情况下存在:
用户界面;
衍生数据;
衍生主要有两种:
Computed Values(计算值):计算值总是可以使用纯函数(pure function)从当前可观察状态中获取;
Reactions(反应):反应指状态变更时需要自动发生的副作用,这种情况下,我们需要实现其读写操作;

react-mobx而言,同样需要两个步骤:

Provider:使用mobx-react提供的Provider将所有stores注入应用;
使用inject将特定store注入某组件,store可以传递状态或action;然后使用observer保证组件能响应store中的可观察对象(observable)变更,即store更新,组件视图响应式更新。

mobx 和 redux 比较

  • redux对象通常不可变(不能直接操作状态对象,而总是在原来状态对象基础上返回一个新的状态对象,这样就能很方便的返回应用上一状态)。而Mobx中可以直接使用新值更新状态对象。
  • redux单一store,mobx多个
  • redux函数式编程,mobx面对对象
  • Redux需要手动追踪所有状态对象的变更,Redux需要手动追踪所有状态对象的变更
  • 运用到react上,mobx-react和react-redux