redux、react-redux、redux-saga之间的关系与用法

React 的官方定义是用于构建用户界面的 JavaScript 库。通常组件内的数据来源于 stateprops,由于 React 的特点之一单向数据流,当渲染一个组件的数据来源于 props 时,通常情况下是 A -> B,但是随着业务复杂度的增加、组件层级增加,可能会变成 A -> B -> C -> D -> E

redux

严格的单向数据流是 Redux 架构的设计核心。

Redux 中的数据流:

  1. 调用 store.dispatch(action),这会触发一个动作。
  2. reducer 函数会接收到上一步发出的 action,并根据 action 中的 type 属性,执行具体的任务
  3. reducer 返回新的 state
  4. 应用中注册了 store.subscribe(listener) 监听器被调用,监听器里可以使用 store.getState() 获取最新的 state

Action

action 是一个描述事件的简单对象,它是改变 storestate 的唯一方法,它通过 store.dispatch() 方法来将 action 传到 store 中。

添加新 todo 任务的 action 可以这样定义:

1
2
3
4
const action = {
type: 'add_todo',
task: '这是一个新的todo任务',
}

action 本质上是一个 JavaScript 对象。我们约定,action 内必须有一个 type 属性,代表要执行的动作。除此之外,action 上的其他属性和结构都可以由你决定。

当应用规模很大时,action type 通常会定义为字符串常量,用一个专用文件存放。如:

1
2
// actionTypes.js
export const ADD_TODO = 'add_todo';

当一个 type 相同的 action 会频繁调用时,我们可以将它封装为一个 Action 创建函数。如上面的 add_todo,我们可以这样封装:

1
2
3
4
5
6
7
8
9
// actions.js
import { ADD_TODO } from 'actionTypes.js';

export function addToDo(task) {
return {
type: ADD_TODO,
task,
}
}

可以这样使用:

1
2
3
4
5
6
7
store.dispatch(addToDo('第一个任务'));
store.dispatch(addToDo('第二个任务'));

// 或者进一步封装
const boundAddToDo = task => store.dispatch(addToDo(task));
// 调用
boundAddToDo('第三个任务');

Reducer

action 决定做什么,reducer 决定怎么做。打个比方:BOSS 今天开会说要参照淘宝做一个电商网站,功能要相同,时间只有1个月。那应该怎么做呢,显然老板不会帮你规划好,这时候就该程序猿上场了,开会+编码+熬夜+秃头,1个月过去了,项目完成。

reducer 是一个纯函数,接收旧的 stateaction,返回一个新的 state。纯函数是什么:不依赖外部环境,不产生副作用,相同的参数,必定返回相同的结果。reducer 必须保持纯净,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算

1
(preState, action) => newState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// reducer.js
import { ADD_TODO } from 'actionTypes.js';

function todoApp(state = {}, action) {
// reducer不能直接改变原state,所以这里可以先深拷贝一份数据
const newState = JSON.parse(JSON.stringify(state));

switch (action.type) {
case ADD_TODO:
const { todos } = newState;
return {
...newState,
todos: [
{ task: action.task, completed: false },
...todos,
]
};
default:
return state;
}
}

当你的 reducer 文件内容很多的时候,你可以使用 reducer 合成。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// reducer.js
import { combineReducers } from 'redux';
import { ADD_TODO } from 'actionTypes.js';

function todos(todos = [], action) {
const newTodos = JSON.parse(JSON.stringify(todos));

switch (action.type) {
case ADD_TODO:
return [
{ task: action.task, completed: false },
...newTodos,
];
default:
return todos;
}
}

const todoApp = combineReducers({
todos,
});

export default todoApp;

Store

store 里存放着整个应用的 state,并将 actionreducer 联系起来。store的其他职责:

  • 提供 getState() 方法获取 state
  • 提供 dispatch(action) 方法更新 state
  • 通过 subscribe(listener) 注册监听器
  • 通过 subscribe(listener) 返回的函数注销监听器
1
2
3
4
5
6
7
8
9
10
11
// store.js
import { createStore } from 'redux';
import todoApp from './reducers';

const INIT_STATE = {
todos: [],
}
// createStore 的第二个参数是可选的,用于设置 state 初始状态
let store = createStore(todoApp, INIT_STATE);

export default store;

React-Redux

React-ReduxRedux 的官方 React 实现。它们的差异具体体现在如何使用全局数据 state 上。

挂载数据到应用

React-Redux 提供了一个 Provider 组件,这个组件的原理就是 React 中的 Context 概念,我们可以将全局数据挂载到这个组件上,然后将整个应用的根组件作为它的子组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
rootElement
);

组件使用全局数据

React-Redux 还提供了一个 connect 方法,它可以将全局数据和组件连接在一起。

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
import React from 'react';
import { connect } from 'react-redux';
import { addToDo, deleteToDo } from './store/action';

class TodoList extends React.Component {
render() {
return (
<ul>
{
this.props.todos.map(item => (
<li key={item.task}>{item.task}</li>
))
}
</ul>
);
}
}

const mapStateToProps = state => {
const { todos } = state;
return {
todos,
}
}

const mapDispatchToProps = dispatch => {
return {
addToDo: task => {
dispatch(addToDo(task));
},
deleteToDo: index => {
dispatch(deleteToDo(index));
},
}
}

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

connect 方法接收4个参数,均为可选参数,它们分别为:

  • mapStateToProps?: Function
  • mapDispatchToProps?: Function | Object
  • mergeProps?: Function
  • options?: Object

上面的代码只用到了 mapStateToProps mapDispatchToProps,所以我们只简单介绍这两个参数。其他参数如果使用,可以去查看官方文档

mapStateToProps 函数,接收全局数据 state 为参数,返回本组件需要用到的数据,并挂载到 props 上。比如:你需要的数据在 state 树上层级比较深,你就可以通过这个方法,把这个数据返回,使用的时候直接到 this.props 上找就行。

mapDispatchToProps 函数,接收 dispatch 为参数,将 dispatch(action) 封装并返回,好处就是发送 action 时更简洁。

Redux-Saga

redux-sage 是一个 redux 的中间件。它的主要作用是处理副作用(如异步请求)。
redux-saga 使用了ES6的 Generator 功能,它有点像 async / await,如果还对它不是很了解,可以阅读《ECMAScript 6 入门》

call:执行异步函数
apply:
put:发出一个 Action,类似 dispatch
take:
takeEvery:监听某个action
takeLatest:只得到最后那个请求的响应
throttle:
watcher:
fork:


参考