官方文档
Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理。
Redux三大原则
- 单一数据源
- redux应用维护了唯一的一个全局状态对象,存储在store中。唯一数据源是一种集中式管理应用状态的方式,便于监控任意时刻应用状态和调试应用。
- 状态不可变
- 任何时候都不能修改应用状态
- 必须通过action修改应用状态,根据action的不同描述,触发dispatch修改唯一数据源。
- Redux 期望所有状态更新都是使用不可变immutable的方式
- 纯函数修改状态
- type 是一个字符串字段,定义action的动作描述,比如”todos/add”、”todos/remove”;把 action 的type 定义为 “域/事件名称”,/前第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。
- action 的其他附加信息,放在第二个参数 payload 中。
action 对象可以如下所示:
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
action creator
action creator 是创建并返回 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:
项目中定义使用
// action types
export const ADD_TODO = "TODOS/ADD_TODO";
export const SET_VISIBILITY_FILTER = "TODOS/SET_VISIBILITY_FILTER";
export const TOGGLE_TODO = "TODOS/TOGGLE_TODO";
//action creators
export const addTodo = (text) => ({
type: ADD_TODO,
text
})
export const setVisibilityFilter = (filter) => ({
type: SET_VISIBILITY_FILTER,
filter
})
export const toggleTodo = (id) => ({
type: TOGGLE_TODO,
id
})
reducer
action用于描述应用发生什么操作,reducer是根据action做出不同响应,决定如何修改应用状态state。
reducer 是一个纯函数,接收当前的 state 和一个 action 对象,共2个参数,返回经过处理后的新的newState,newState必须是一个全新的数据对象,如果state是引用数据类型,需要在原来state的基础上复制出来一个对象并进行修改,通常使用es6的扩展运算符或Object.assign()。
reducer函数签名: (state, action) => newState;
reducer使用规则
- 仅使用state和action计算新的状态值
- 禁止直接修改state
- 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码,必须是纯函数。
- 如果有异步逻辑加入,需要使用中间件。
reducer 的小例子
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// 检查 reducer 是否关心这个 action
if (action.type === 'counter/increment') {
// 如果是,复制 `state`
return {
...state,
// 使用新值更新 state 副本
value: state.value + 1
}
}
// 返回原来的 state 不变
return state
}
拆分reducer
随着应用的复杂,会把reducer拆分成多个reducer
// 定义2个reducer,并且设置初始状态
// reducer todos的初始状态为空数组
function todos(state = [], action){
// ...
}
// reducer visibilityFilter的初始状态为 show_all
function visibilityFilter(state = "show_all", action){
// ...
}
合并reducer
Redux提供combineReducers 方法,方便把多个拆分的reducer组合到一起。
import { combineReducers } from "redux";
const todoApp = combineReducers({
todos,
visibilityFilter
})
// ==> 等价于
function todoApp(state = {}, action){
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}
store
Redux应用唯一状态数据源store对象
- store 是通过传入 reducer 创建,代码第8行。const store = createStore(reducer);
- getState 的方法,返回当前状态值。
- dispatch(action) 方法,发送更新状态的意图
- subscribe(listener) 方法,注册监听函数,监听应用状态的改变
一个Redux应用只有一个store,store保存唯一数据源。store依赖reducer创建。
import { createStore } from "redux";
import reducer from './reducers'
// 将reducer作为参数传入
let store = createStore(reducer);
// 创建store时,还可以设置初始状态
// let store = createStore(reducer, initialState);
store创建完成,可以通过getState() 获取当前应用的状态state
const state = store.getState();
修改state时,通过store的dispatch方法,发送action。
// 定义action
function addTodo(text){
return {type: "ADD_TODO", text}
}
// 发送action
store.dispatch(addTodo('todo ...'))
使用subscribe监听函数,可以观察到state的变化
let unsubscribe = store.subscribe(()=>console.log(store.getState()));
每次应用状态更新,最新的应用状态就会被打印出来;需要取消监听是,可以直接调用unsubscribe();
dispatch
store对象有一个dispatch方法,用来发送action,然后执行reducer更新state对象。dispatch是更新store数据变化的唯一方法。
dispatch一个action可以理解为触发了一个事件。发生一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当reducer收到关注的 action 后,它就会更新 state 作为响应。
通常调用 action creator 来调用 action
const increment = () => {
return {
type: 'counter/increment'
}
}
store.dispatch(increment())
console.log(store.getState())
// {value: 2}
redux数据流程示意图
在React中使用Redux
react和redux并无直接关联,通过react-redux进行关联。
connect函数
react-redux提供了connect函数,用于把react组件和redux的store连接起来,生成容器组件。
代码示例
import { connect } from "react-redux";
import TodoList from '../components/TodoList'
const TodoList = connect()(TodoList);
创建出TodoList组件,可以把TodoList和Redux连接起来。创建处理的TodoList组件需要承担2项任务:
- 从Redux的store中获取展示组件所需要的应用状态
- 把展示组件的状态变化同步到Redux的store中
通过为connect传递2个参数可以让TodoList具备这两个功能。
import { connect } from "react-redux";
import TodoList from '../components/TodoList';
const mapStateToProps = (state) => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = {
onTodoClick: toggleTodo
}
const TodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList);
mapStateToProps和mapDispatchToProps都是函数
- mapStateToProps负责从全局应用状态state取出所需数据,映射到展示组件的props
- mapDispatchToProps负责把需要用到的action映射到展示组件的props上
mapStateToProps
mapStateToProps作用把state转换成props。
state就是Redux store中保存的应用状态,会作为参数传递给mapStateToProps,props就是展示组件中的props
每当store的state更新,mapStateToProps都会重新执行,重新计算并返回props,从而触发组件的重新渲染。const mapStateToProps = (state) => ({
todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
mapDispatchToProps
mapDispatchToProps接收store.dispatch 方法作为参数,返回 给组件用于修改state的函数。 ```javascript const toggleTodo = (id) => ({ type: ‘TOGGLE_TODO’, id })
const mapDispatchToProps = (dispatch) => { onTodoClick: (id) => dispatch(toggleTodo(id)) }
<a name="VqCPZ"></a>
### Provider
通过connect函数创建出的容器组件,怎么获取到Redux的store呢?<br />react-redux提供Provider组件,Provider简化代码如下
```javascript
// Provider 内部实现也是依赖react 的context
class Provider extends Component{
getChildContext(){
return { store: this.props.store}
}
render(){
return this.props.children
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
Provider组件正是通过context 把store传递给子组件,使用Provider时,把它做为根组件,这样内层的任意组件才可以从context 中获取store对象。
项目中使用Provider
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import reducer from './reducers'
const store = createStore(reducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
redux的中间件
Redux中间件和node框架express类似,redux的action可类比web框架接收到请求,reducer可类比web框架的业务逻辑层,因此redux中间价代表action在到达reducer前的处理程序。
实际上,redux中间件就是一个函数。
redux中间件增强了store功能,可以利用中间件为action添加一些通用功能,例如日志输入、异常获取,可以通过store.dispatch日志输出的功能:
let next = store.dispatch
store.dispatch = function dispatchAndLog(action){
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
}
上面示例代码重新定义store.dispatch,在发送action前后都添加了日志输出,对store.dispatch进行改造,在发送action和执行reducer这两步之间添加其他功能。
redux-logger中间件使用
import {applyMiddleware, createStore} from 'redux';
import logger from 'redux-logger';
import reducer from './reducers'
const store = createStore(
reducer,
applyMiddleware(logger)
)
代码从redux-logger中引入日志中间件logger,然后将它放入applyMiddleware函数中并传给createStore,完成store.dispatch 功能的加强。
applyMiddleware函数
import compose from './compose'
export default function applyMiddleware(...middlewares) {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args),
}
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
export default function compose(...funcs) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
applyMiddleware把接收到的中间件放入数组 chain中,通过 compose(…chain)(store.dispatch)定义加强版的dispatch方法。
每个中间件都接收一个包含getState和dispatch的参数对象,在利用中间件执行异步操作时,会使用这两个方法
redux-thunk异步操作中间件
异步操作在web应用中必不可少,最常见的发送网络请求。由于redux的工作流,发送action,reducer处理接收到的action,reducer返回新的state。这个流程不涉及异步操作,需要使用Redux 中间件处理异步操作。
redux-thunk时处理异步操作最常用的中间件。使用redux-thunk的代码如下:
import {applyMiddleware, createStore} from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers'
const store = createStore(
reducer,
applyMiddleware(thunk)
);
现在定义异步action模拟向服务器请求数据
// 异步action
function getData(url){
return function (dispatch){
return fetch(url)
.then(
response => response.json(),
error => console.log('error', error)
)
.then(json => dispatch({type:'recive_data', data: json}))
}
}
// 发送action
store.dispatch(getData("http://xxx"))
如果不使用redux-thunk中间件,上面代码会报错,因为store.dispatch只能接收普通JavaScript对象代表的action,使用redux-thunk后,store.dispatch就能接收函数作为参数。
异步action会先经过redux-thunk的处理,请求返回后,再次发送一个action: {type:’recive_data’, data: json}
这时的action就是普通JavaScript对象,处理流程和不使用中间件的流程一样。
redux-saga中间件
redux-saga也是处理异步的中间件。
学习文档
常用API:
- take:创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 pattern 匹配的 action 之前,Generator 将暂停。
- takeEvery: 允许处理并发的 action, 但是不会对多个任务的响应进行排序,并且不保证任务将会以它们启动的顺序结束。
- takeLatest: 如果要对响应进行排序,可以使用takeLatest。在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。takeLatest 将会在后台启动一个新的 saga 任务。
获取远程数据示例
```javascript import { all, call, put, takeEvery } from ‘redux-saga/effects’; import { LOAD_TODO_LIST, RENDER_TODO_LIST } from ‘../actions’;
export function* fetchToDoList() { const endpoint = ‘https://gist.githubusercontent.com/brunokrebs/‘ + ‘f1cacbacd53be83940e1e85860b6c65b/raw/to-do-items.json’; const response = yield call(fetch, endpoint); const data = yield response.json(); yield put({ type: RENDER_TODO_LIST, toDoList: data }); }
export function* loadToDoList() { yield takeEvery(LOAD_TODO_LIST, fetchToDoList); }
export default function* rootSaga() { yield all([loadToDoList()]); }
<a name="YXRAn"></a>
#### 案例:
首先创建一个关联react和redux的组件
```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider, connect } from "react-redux";
// 引入被增强的store,即添加了 applyMiddleware(sagaMiddleware)
import store from "./store";
import actions, { addActionCreator, addAsyncActionCreator } from "./store/actions";
function Counter(props) {
return (
<div>
<p>{props.count}</p>
<button onClick={() => props.add(3)}>add</button>
<button onClick={() => props.addAsync()}>async increment</button>
</div>
);
}
const mapStateToProps = (state) => state;
const mapDispatchToProps = (dispatch) => {
console.log('click');
return {
add: (payload) => dispatch(addActionCreator(payload)),
addAsync: () => dispatch(addAsyncActionCreator()),
// add: (payload) => dispatch(actions.add(payload)),
};
};
// 通过connect,连接react组件和redux数据
const NEWCounter = connect(mapStateToProps, mapDispatchToProps)(Counter);
// 组件渲染
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// 数据提供 Provider
<Provider store={store}>
<NEWCounter />
</Provider>
);
为了运行Saga,我们需要:
- 创建一个 Saga middleware 和要运行的 Sagas
- 将这个 Saga middleware 连接至 Redux store ```javascript import { createStore, applyMiddleware} from “redux”; import createSagaMiddleware from “redux-saga”; import reducer from “./reducer”; import {rootSaga} from “./saga”;
// 创建sagaMiddleware 实例,该实例被传递到applyMiddleware let sagaMiddleware = createSagaMiddleware(); // 创建全局store,使用applyMiddleware,对store进行加强 let store = createStore(reducer, applyMiddleware(sagaMiddleware)); // sagaMiddleware 的run方法,根saga sagaMiddleware.run(rootSaga) export default store;
```javascript
import * as actionTypes from "./action-types";
const reducer = (state = { count: 0 }, action) => {
switch (action.type) {
case actionTypes.ADD:
return { count: state.count + action.payload };
case actionTypes.INCREMENT:
return { count: state.count + 1 };
default:
return state;
}
};
export default reducer;
import { put, take, takeEvery, call, all, delay } from "redux-saga/effects";
import * as actionTypes from "./action-types";
export function* incrementAsync() {
yield delay(2000);
yield put({ type: "INCREMENT" });
}
export function* watchIncrementAsync() {
yield takeEvery(actionTypes.INCREMENT_ASYNC, incrementAsync);
}
function* addAsync() {
// 延时2秒
yield delay(2000);
// 触发reducer更新
yield put({ type: "ADD", payload: 5 });
}
function* watchAddAsync() {
// takeEvery持续监听每次 action的触发
yield takeEvery(actionTypes.ASYNC_ADD, addAsync);
}
// 创建rootSaga
export function* rootSaga() {
for (let i = 0; i < 3; i++) {
console.log(`wait ${actionTypes.ASYNC_ADD} action`);
// 事件订阅者 take,等待事件的触发,take只是一次
const action = yield take(actionTypes.ASYNC_ADD);
// console.log(action, "wait ok");
// 触发take 事件后,执行 put 派发事件
yield put({ type: actionTypes.ADD, payload: action.payload });
console.log("continue run");
}
console.log("for game over,执行完成后,后面的程序才能执行");
yield all([call(watchAddAsync), call(watchIncrementAsync)]);
}
export const ADD = "ADD";
export const ASYNC_ADD = "ASYNC_ADD";
export const INCREMENT = "INCREMENT";
export const INCREMENT_ASYNC = "INCREMENT_ASYNC";