Redux01 入门

Redux入门学习。

Redux是一个可预测状态的JavaScript容器

是否需要使用Redux

Redux的出现是为了解决React的两个问题:

1
2
1. 代码结构
2. 组件之间的通信

是否使用Redux,取决于React应用是否存在上面的问题需要解决,一般来说,如果UI层比较简单,没有很多的交互,那么Redux就不是必须的,但是如果你面临的是下面的某些场景,那么就有必要考虑使用Redux了:

1
2
3
4
5
1. 用户的使用方式复杂
2. 不同身份的用户有不同的使用方式(比如普通用户和管理员)
3. 多个用户可以写作
4. 与服务器大量交互,或者使用了Websocket
5. view需要从多个来源获取数据

上面的情况才是Redux的适用场景:多交互、多数据源

从组件角度来看,如果组件有一下需求,可以考虑使用Redux:

1
2
3
4
1. 某个组件的状态需要共享
2. 某个状态需要在任何地方都能够拿到(全局状态)
3. 一个组件需要改变全局状态
4. 一个组件需要改变另一个组件的状态

Redux提供了一个统一的位置和机制来管理状态、组织代码结构,是Web架构的一种解决方案

设计思想

Redux的设计思想:

1
2
1. Web应用是一个状态机,视图与状态是一一对应的
2. 所有的状态保存在一个对象里面

这是flux单向数据流图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                 _________               ____________               ___________
| | | | | |
| Action |------------▶| Dispatcher |------------▶| callbacks |
|_________| |____________| |___________|
▲ |
| |
| |
_________ ____|_____ ____▼____
| |◀----| Action | | |
| Web API | | Creators | | Store |
|_________|----▶|__________| |_________|
▲ |
| |
____|________ ____________ ____▼____
| User | | React | | Change |
| interactions |◀--------| Views |◀-------------| events |
|______________| |___________| |_________|

其中的每一个步骤和概念都是下面要介绍的一个部分,其实没有什么太多新的东西,更多是一种编程的范式和思想。

在flux的流程中,flux确保所有Action首先通过一个Dispatcher发送给Store,由Reducer计算后通知所有的监听器

概念

首先创建一个React应用,然后安装Redux

1
npm install redux --save

Store

Store是保存数据的地方,可以把它看成一个容器。==整个应用应该只有一个Stroe==。

1
2
import { createStore } from 'redux';
const store = createStore(fn);

Redux提供了createStore函数,来生成Stroe,这个函数接受另一个函数作为参数,这个函数就是下面要提到的reducer,返回新生成的Stroe对象。

createStore传Reducer的过程就是给Redux绑定Action处理函数(也就是Reducer)的过程

State

Store对象包含所有数据,想要得到某一时刻(状态)下的数据,新需要对Store生成快照,生成的快照数据就是State

当前时刻的State,可以通过Store.getState获得:

1
2
3
4
import { createStore } from 'redux';
const store = createStore(fn);

const state = store.getState();

Redux中,一个State对应一个View,只要State相同,View就相同,反之亦然。

Action

State的变化就会导致View的变化,但用户接触不到State,用户通过View发出通知,告诉State要发生变化了,这个通知就是Action。

Action是一个对象,必须包含一个名为type的属性,用来标识Action的名称,其他属性可以自由设置,可以参考这个规范进行设置。

1
2
3
4
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
}

可以这样理解,Action描述当前发生的事情,它是唯一改变State的方法,将数据从View运送到Store。

Action Creator

View要发送多少种消息,就有多少种Action,如果都手写会很麻烦。可以定义一个函数来生成Action,这个函数就是Action Creator

1
2
3
4
5
6
7
8
const ADD_TODO = '添加TODO';

funciton addTodo (text) {
return {
type: ADD_TODO,
text
}
}

Dispatch

有了Action,还需要一种行为将Action由View传递到Store,这种行为就是dispatch,store.dispatch是View发出Action的唯一方法

1
2
3
4
5
6
7
8
9
import { createStore } from 'redux';
const store = createStore(fn);

const action = {
type: '添加TODO',
payload: 'Learn Redux'
}

store.dispatch(action)

store.dispatch接受一个Action对象作为参数,并将它发送给Store,结合上面的Action Creator,可以改写为

1
store.dispatch(addTodo('Learn Redux'))

Reducer

Store收到Action后,需要对Action进行处理,返回一个新的State,这样View才会发生变化,这种State的计算过程就是Reducer

Reducer是一个函数,它接受当前State和一个Action作为参数,返回一个新的State:

1
2
3
4
const reducer = function (state, action) {
// ...
return new_state;
};

store.dispatch会自动触发Reducer的自动执行(因为在使用CreateStore时将Reducer作为参数传递给了Store)

1
2
import { createStore } from 'redux';
const store = createStore(reducer);

==Reducer函数最重要的特征是,它是一个纯函数==,也就是说只要是同样的输入,必定得到同样的输出。

纯函数必须遵守以下的约束:

1
2
3
- 不得改写参数
- 不得调用系统I/O的API
- 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不同的结果

Reducer函数中不能改变State对象,必须返回一个==全新的对象==,参考下面的写法:

1
2
3
4
5
6
7
8
9
10
11
// State 是一个对象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}

// State 是一个数组
function reducer(state, action) {
return [...state, newItem];
}

使用上面的ES7的对象展开进行拷贝时,只是浅拷贝,如果数据结构更复杂或者是嵌套的,那在处理State更新的时候可能要考虑一些不同的做法,可以考虑使用ImmutableJS,Redux对此是全无预设方式的,记住它只是一个状态的容器。

任何时候,与某个View对应的State总是一个不变的对象

这里有个常见模式:在Reducer里用switch来响应对应的Action :

1
2
3
4
5
6
7
8
9
10
11
12
13
const reducer3 = function (state = {}, action) {
console.log('reducer_3 was called with state', state, 'and action', action)

switch (action.type) {
case 'SAY_SOMETHING':
return {
...state,
message: action.value
};
default:
return state;
}
};

switch的时候, ==永远==不要忘记放个default来返回State,否则,你的Reducer可能会返回undefined(等于你的State就丢了)

Subscribe

现在,用户在View层,通过dispatch发出了一个Action到Stroe,触发了对应的Reducer返回了一个新的State,但是这个State和View之间还需要关联起来,才能让视图进行封信

通过store.subscribe可以设置监听函数,一旦State发生变化,设置的监听函数就会自动执行

1
2
3
4
import { createStore } from 'redux';
const store = createStore(reducer);

store.subscribe(listener);

所以需要吧View的更新函数(对于React就是组件的render方法或setState方法)放到listener,就可以实现View根据State对象的变化而自动更新渲染。

store.subscribe返回一个函数,调用这个函数可以解除监听

1
2
3
4
5
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);

unsubscribe();

Store的实现

Store设对象提供了三种基本方法:

1
2
3
- store.getState()
- store.dispatch()
- store.subscribe()

Store对象是由Redux提供的createStore方法创造的,这个方法除了接受一个Reducer作为第一个参数外,还接受第二个参数,表示State的初始状态,==这个状态会覆盖Reducer函数的默认参数==

下面是createStore的简单实现,利用了闭包的原理(也证明,如果声明两个Store对象,其中保存的State对象是相互独立的)

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
const createStore = reducer => {
let state = reducer(undefined, {});
let listeners = [];

const getState = () => state;

const dispatch = action => {
reducer(state, action);
listeners.forEach(fn => {
fn()
})
};

const subscribe = listener => {
listeners.push(listener);
return () => {
listeners.filter(fn => fn !== listener)
}
};

return {
getState,
dispatch,
subscribe
}
};

Reducer的拆分

Reducer函数负责生成State对象,但是由于整个应用只有一个State对象,包含所有数据,对于大型应用来说,这个State对象会很大

看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};

三种Action分别改变State的三个属性:

1
2
3
- ADD_CHAT:chatLog属性
- CHANGE_STATUS:statusMessage属性
- CHANGE_USERNAME:userName属性

三个属性之间没有联系,因此可以将Reducer函数拆分,不同的函数负责处理不同的属性(即部分state),然后再合并为一个大的Reducer即可

1
2
3
4
5
6
7
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
};

拆分后,每一个小的函数负责修改对应的属性,返回值都是完整的State对象

一开始我的理解有一点问题,以为返回一个对象,对象的内容是方法,根据传入的action.type的值来确定执行哪个方法。并不是这样,返回的对象的每一个内容都是State对象的一个属性,当一个Action发送到Stroe后,对象中的每一个子Reducer函数都会被执行一次,这样才能返回完整的State对象

这个时候,每个属性对应的子Reducer内部还是要根据action.type来判断具体逻辑的,否则会应用在所有属性上

这种拆分与React应用的结构相吻合,一个React根组件由很多子组件构成,子组件与Reducer完全可以对应。

Redux提供了combineReducers方法,用于Reducer的拆分,只要定义各个子Reducer函数,然后调用这个方法,将它们合成一个大的Reducer:

1
2
3
4
5
6
7
8
9
mport { combineReducers } from 'redux';

const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})

export default chatReducer;

combineReducers产生一个整体的Reducer函数,根据State的key,分别执行子Reducer,并将返回的记过合并成为一个大的State对象

combineReducers的简单实现,注意返回的应该是一个函数(与常规的Reducer相同),给每个子Reducer传递的第一个参数不应该是整个State对象,而是对应的子对象State[key]

1
2
3
4
5
6
const combineReducer = reducers => (state = {}, action) => {
Object.keys(reducers).reduce((newState, key) => {
newState[key] = reducers[key](state[key], action);
return newState
}, {})
}

可以把所有子Reducer放在一个文件里面,然后统一引入

1
2
3
4
import { combineReducers } from 'redux'
import * as reducers from './reducers'

const reducer = combineReducers(reducers)

工作流程

之前接触过Vuex,感觉里面一些概念是非常类似的,首先来看一下Redux的工作流程

bg2016091802.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 用户发出Action
store.dispatch(action)

// 2. Store自动调用Reducer,并传入两个参数,当前State和收到的Action,返回一个新的State
let newState = reducer(previousState, atcion)

// 3. 如果返回的新的State发生了变化,Store就会调用监听函数
store.subscribe(listener)

// 4. 监听函数listener中可以通过getState获取最新的State,在这里可以触发重新渲染View
function listener () {
const newState = store.getState();
component.setState(newState)
}

实例:计数器

我将阮一峰老师的例子稍微改写了一下,只是形式上采用了Class组件的形式

首先在demo1.js组件中,引入Redux

1
const store = createStore(reducer);

reducer是从./reducers/index导出的一个经由combineReducers合成的Reducer(reducer2没什么用,只是为了练习而引入的)

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
import { combineReducers } from 'redux'

const reducer1 = (state = 0, action) => {
console.log('reducer1 was called with state', state, 'and action', action);
switch (action.type) {
case 'INCREMENT': {
return +state + 1
}
case 'DECREMENT': {
return +state - 1
}
case 'CHANGE': {
return +action.payload
}
default: {
return +state
}
}
};

const reducer2 = (state = { test: 'go' }, action) => {
if (action.type === 'INCREMENT') {
console.log('reducer2 was called with state', state, 'and action', action);
}
return state
};

const reducer = combineReducers({
val1: reducer1,
val2: reducer2
});

export default reducer

<Demo1>中,<Count>是一个函数式组件,只负责表现,具体的逻辑在<Demo1>中,通过store.getState来为this.state中的变量赋值,当用户点击按钮而使用dispatch(action)发出了Action,State变化,会触发监听函数变化,监听函数中再来调用setState来触发视图的更新

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 Count = ({ value, onIncrement, onDecrement, onChange }) => {
return (
<div>
<button onClick={ onIncrement }>+</button>
<span>Now, the count is { value }</span>
<button onClick={ onDecrement }>-</button>
<input type="number" onInput={ onChange } />
</div>
)
};

export default class Demo1 extends Component {
constructor(props) {
super(props);
this.state = {
val1: store.getState().val1,
val2: store.getState().val2
}
}

render() {
const value = this.state.val1;
const value2 = this.state.val2;

const ACTIONS = {
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
CHANGE: 'CHANGE'
};

store.subscribe(() => {
this.setState({
val1: store.getState().val1
})
});

return (
<Count value={ value }
value2={ value2 }
onIncrement={ () => store.dispatch({ type: ACTIONS.INCREMENT }) }
onDecrement={ () => store.dispatch({ type: ACTIONS.DECREMENT }) }
onChange={ (e) => store.dispatch({
type: ACTIONS.CHANGE,
payload: e.target.value
}) } />
)
}
};

参考