在 React 的学习和开发中,如果 state (状态)变得复杂时(例如一个状态需要能够在多个 view 中使用和更新),使用 Redux 可以有效地管理 state,使 state tree 结构清晰,方便状态的更新和使用。
当然,Redux 和 React 并没有什么关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。只是对我来说目前主要需要在 React 中使用,所以在这里和 React 联系起来便于理解记忆。

数据流

Action

只是描述 state (状态)更新的动作,即“发生了什么”,并不更新 state。

const ADD_TODO = 'ADD_TODO'{type: ADD_TODO,text: 'Build my first Redux app'
}
  • type:必填,表示将要执行的动作,通常会被定义成字符串常量,尤其是大型项目。
  • 除了 type 外的其他字段:可选,自定义,通常可传相关参数。例如上面例子中的 text。

Action 创建函数

简单返回一个 Action:

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

dispatch Action:

dispatch(addTodo(text))
// 或者创建一个 被绑定的 action 创建函数 来自动 dispatch
const boundAddTodo = text => dispatch(addTodo(text))
boundAddTodo(text)

帮助生成 Action 创建函数的库(对减少样板代码有帮助):

  • redux-actions
  • redux-act

redux-actions

createAction(s)handleAction(s)combineActions

createAction(type,payloadCreator = Identity, // function/undefined/null,默认使用 lodash 的 Identity; 如果传入 Error,则不会调用 payloadCreator 处理 Error,而是设置 action.error 为 true?metaCreator // 用来保存 payload 以外的其他数据
)const addTodo = createAction('ADD_TODO',text => ({text: text.trim(), created_at: new Date().getTime()}),() => ({ admin: true })
);expect(addTodo('New Todo')).to.deep.equal({type: 'ADD_TODO',payload: {text: 'New Todo',created_at: 1551322911779},meta: { admin: true }
});const error = new TypeError('error');
expect(addTodo(error)).to.deep.equal({type: 'ADD_TODO',payload: error,error: true
});
createActions(actionMap, // {type => payloadCreator / [payloadCreator, metaCreator] / actionMap}?...identityActions, // 字符串类型的参数列表,表示一组使用 Identity payloadCreator 的 actions?options // 定义 type 前缀:{ prefix, namespace } prefix 前缀字符串,namespace 前缀和 type 之间的分隔符(默认为 /)
)const actionCreators = createActions({TODO: {ADD: todo => ({ todo }), // payload creatorREMOVE: [todo => ({ todo }), // payload creator(todo, warn) => ({ todo, warn }) // meta creator]},COUNTER: {INCREMENT: [amount => ({ amount }), amount => ({ key: 'value', amount })],DECREMENT: amount => ({ amount: -amount }),SET: undefined // given undefined, the identity function will be used}},'UPDATE_SETTINGS',{prefix: 'app',namespace: '-'}
);expect(actionCreators.todo.remove('Todo 1', 'warn: xxx')).to.deep.equal({type: 'app-TODO-REMOVE',payload: { todo: 'Todo 1' },meta: { todo: 'Todo 1', warn: 'warn: xxx' }
});expect(actionCreators.updateSettings({ theme: 'blue' })).to.deep.equal({type: 'app-UPDATE_SETTINGS',payload: { theme: 'blue' }
})

redux-actions 也能帮助生成 reducer,

handleAction(type,reducer | reducerMap = Identity,defaultState,
)handleAction('ADD_TODO',(state, action) => ({...state,{text: action.payload.text,completed: false}}),{ text: '--', completed: false },
);const reducer = handleAction('INCREMENT', {next: (state, { payload: { amount } }) => ({ ...state, counter: state.counter + amount }),throw: state => ({ ...state, counter: 0 }),
}, { counter: 10 });expect(reducer(undefined, increment(1)).to.deep.equal({ counter: 11 });
expect(reducer({ counter: 5 }, increment(1)).to.deep.equal({ counter: 6 });
expect(reducer({ counter: 5 }, increment(new Error)).to.deep.equal({ counter: 0 });
handleActions(reducerMap, defaultState[, options])handleActions({INCREMENT: (state, action) => ({counter: state.counter + action.payload}),DECREMENT: (state, action) => ({counter: state.counter - action.payload})},{ counter: 0 }
);// Map
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';handleActions(new Map([[INCREMENT,(state, action) => ({counter: state.counter + action.payload})],[DECREMENT,(state, action) => ({counter: state.counter - action.payload})]]),{ counter: 0 }
);const increment = createAction(INCREMENT);
const decrement = createAction(DECREMENT);const reducer = handleActions(new Map([[increment,(state, action) => ({counter: state.counter + action.payload})],[decrement,(state, action) => ({counter: state.counter - action.payload})]]),{ counter: 0 }
);

当多个 action 有相同的 reducer 时,可以使用 combineActions,

combineActions(...types) // types: strings, symbols, or action creatorsconst { increment, decrement } = createActions({INCREMENT: amount => ({ amount }),DECREMENT: amount => ({ amount: -amount })
});const reducer = handleActions({[combineActions(increment, decrement)]: (state,{ payload: { amount } }) => {return { ...state, counter: state.counter + amount };}},{ counter: 10 }
);

Reducer

说明在发起 action 后 state 应该如何更新。
是一个纯函数:只要传入参数相同,返回计算得到的下一个 state 就一定相同。
(previousState, action) => newState
注意,不能在 reducer 中执行的操作:

  • 修改传入的参数
  • 执行有副作用的操作,如 API 请求和路由跳转
  • 调用非纯函数,如 Date.now() 或 Math.random()
import { combineReducers } from 'redux'
import {ADD_TODO,TOGGLE_TODO,SET_VISIBILITY_FILTER,VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFiltersfunction visibilityFilter(state = SHOW_ALL, action) {switch (action.type) {case SET_VISIBILITY_FILTER:return action.filterdefault:return state}
}function todos(state = [], action) {switch (action.type) {case ADD_TODO:return {...state,{text: action.text,completed: false}}case TOGGLE_TODO:return state.map((todo, index) => {if (index === action.index) {return Object.assign({}, todo, {completed: !todo.completed})}return todo})default:return state}
}const todoApp = combineReducers({visibilityFilter,todos
})export default todoApp

Store

Redux 应用只有一个单一的 store。

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp,[preloadedState], // 可选,state 初始状态enhancer
)
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers/index'export default function configureStore() {const store = createStore(reducer,compose(applyMiddleware(thunk),DevTools.instrument()));return store;
}

react-redux

connect() 方法(mapStateToPropsmapDispatchToProps

替代 store.subscribe(),从 Redux state 树中读取部分数据,并通过 props 提供给要渲染的组件。

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './actions';class App extends Component {handleAddTodo = () => {const { actions } = this.props;actions.addTodo('Create a new todo');}render() {const { todos } = this.props;return (<div><Button onClick={this.handleAddTodo}>+</Button><ul>{todos.map(todo => (<Todo key={todo.id} {...todo} />))}</ul></div>);}
}function mapStateToProps(state) {return {todos: state.todos};
}function mapDispatchToProps(dispatch) {return {actions: bindActionCreators({addTodo: actions.addTodo}, dispatch)}
}export default connect(mapStateToProps,mapDispatchToProps
)(App);

Provider 组件

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'
import App from './components/App'render(<Provider store={configureStore()}><App /></Provider>,document.getElementById('root')

API 请求

一般情况下,每个 API 请求都需要 dispatch 至少三种 action:

  • 通知 reducer 请求开始的 action { type: 'FETCH_POSTS_REQUEST' }
    reducer 可能会 {...state, isFetching: true}
  • 一种通知 reducer 请求成功的 action { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
    reducer 可能会 {...state, isFetching: false, data: action.response}
  • 一种通知 reducer 请求失败的 action { type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
    reducer 可能会 {...state, isFetching: false, error: action.error}

使用 middleware 中间件实现网络请求:

  • redux-thunk
  • redux-saga

redux-thunk

通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk。

function shouldFetchPosts(state) {if (state.posts.isFetching) {return false;}return true;
}export function fetchPosts() {return (dispatch, getState) => {if (!shouldFetchPosts(getState())) {return Promise.resolve();}dispatch({ type: 'FETCH_POSTS_REQUEST' });return fetch(postApi).then(response => {const data = response.json();return dispatch({type: 'FETCH_POSTS_SUCCESS', data});});}
}
...actions.fetchPosts().then(() => console.log(this.props.posts))...function mapStateToProps(state) {return {posts: state.posts};
}function mapDispatchToProps(dispatch) {return {actions: bindActionCreators({fetchPosts}, dispatch)}
}...

redux-saga

声明式 vs 命令式:

  • DOM: jQuery / React
  • Redux effects: redux-thunk / redux-saga

实现获取用户信息的两种方式对比:

  • redux-thunk

    <div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>function loadUserProfile(userId) {return dispatch => fetch(`http://data.com/${userId}`).then(res => res.json()).then(data => dispatch({ type: 'USER_PROFILE_LOADED', data }),err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err }));
    }
  • redux-saga

    <div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>  function* loadUserProfileOnNameClick() {yield* takeLatest("USER_NAME_CLICKED", fetchUser);
    }function* fetchUser(action) {try {const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)yield put({ type: 'USER_PROFILE_LOADED', userProfile })} catch(err) {yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })}
    }

比较看来,使用 redux-saga 的代码更干净清晰,方便测试。

redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。

class UserComponent extends React.Component {...onSomeButtonClicked() {const { userId, dispatch } = this.propsdispatch({type: 'USER_FETCH_REQUESTED', payload: {userId}})}...
}

sagas.js

import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...'// worker Saga: will be fired on USER_FETCH_REQUESTED actions
function* fetchUser(action) {try {const user = yield call(Api.fetchUser, action.payload.userId);yield put({type: "USER_FETCH_SUCCEEDED", user: user});} catch (e) {yield put({type: "USER_FETCH_FAILED", message: e.message});}
}
// or
function fetchUserApi(userId) {return Api.fetchUser(userId).then(response => ({ response })).catch(error => ({ error }))
}function* fetchUser(action) {const { response, error } = yield call(fetchUserApi, action.payload.userId);if (response) {yield put({type: "USER_FETCH_SUCCEEDED", user: user});} else {yield put({type: "USER_FETCH_FAILED", message: e.message});}
}/*Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action.Allows concurrent fetches of user.
*/
function* mySaga() {yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}/*Alternatively you may use takeLatest.Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" getsdispatched while a fetch is already pending, that pending fetch is cancelledand only the latest one will be run.
*/
function* mySaga() {yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}export default mySaga;/**** 测试: ****/
const iterator = fetchUser({ payload: {userId: 123} })// 期望一个 call 指令
assert.deepEqual(iterator.next().value,call(Api.fetchUser, 123),"fetchProducts should yield an Effect call(Api.fetchUser, 123)"
)// 创建一个假的响应对象
const user = {}// 期望一个 dispatch 指令
assert.deepEqual(iterator.next(user).value,put({ type: 'USER_FETCH_SUCCEEDED', user }),"fetchProducts should yield an Effect put({ type: 'USER_FETCH_SUCCEEDED', user })"
)// 创建一个模拟的 error 对象
const error = {}// 期望一个 dispatch 指令
assert.deepEqual(iterator.throw(error).value,put({ type: 'USER_FETCH_FAILED', error }),"fetchProducts should yield an Effect put({ type: 'USER_FETCH_FAILED', error })"
)

main.js

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'import reducer from './reducers'
import mySaga from './sagas'// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(reducer,applyMiddleware(sagaMiddleware)
)// then run the saga
sagaMiddleware.run(mySaga)// render the application

路由跳转

一般使用 react-router,与 redux 无关。如果想要使用 redux 管理 route 状态,可以使用 connect-react-router (history -> store -> router -> components)

dva 框架

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
通过 reducers, effects 和 subscriptions 组织 model:
User Dashboard 的 model 配置,

import * as usersService from '../services/users';export default {namespace: 'users',state: {list: [],total: null,page: null,},reducers: {save(state, { payload: { data: list, total, page } }) {return { ...state, list, total, page };},},effects: {*fetch({ payload: { page = 1 } }, { call, put }) {const { data, headers } = yield call(usersService.fetch, { page });yield put({type: 'save',payload: {data,total: parseInt(headers['x-total-count'], 10),page: parseInt(page, 10),},});},*remove({ payload: id }, { call, put }) {yield call(usersService.remove, id);yield put({ type: 'reload' });},*patch({ payload: { id, values } }, { call, put }) {yield call(usersService.patch, id, values);yield put({ type: 'reload' });},*create({ payload: values }, { call, put }) {yield call(usersService.create, values);yield put({ type: 'reload' });},*reload(action, { put, select }) {const page = yield select(state => state.users.page);yield put({ type: 'fetch', payload: { page } });},},subscriptions: {setup({ dispatch, history }) {return history.listen(({ pathname, query }) => {if (pathname === '/users') {dispatch({ type: 'fetch', payload: query });}});},},
};

action 添加前缀 prefix,

function prefix(obj, namespace, type) {return Object.keys(obj).reduce((memo, key) => {const newKey = `${namespace}${NAMESPACE_SEP}${key}`;memo[newKey] = obj[key];return memo;}, {});
}function prefixNamespace(model) {const {namespace,reducers,effects,} = model;if (reducers) {if (isArray(reducers)) {model.reducers[0] = prefix(reducers[0], namespace, 'reducer');} else {model.reducers = prefix(reducers, namespace, 'reducer');}}if (effects) {model.effects = prefix(effects, namespace, 'effect');}return model;
}

reducer 处理,

function getReducer(reducers, state, handleActions) {// Support reducer enhancer// e.g. reducers: [realReducers, enhancer]if (Array.isArray(reducers)) {return reducers[1]((handleActions || defaultHandleActions)(reducers[0], state));} else {return (handleActions || defaultHandleActions)(reducers || {}, state);}
}

saga,

import * as sagaEffects from 'redux-saga/lib/effects';
import {takeEveryHelper as takeEvery,takeLatestHelper as takeLatest,throttleHelper as throttle,
} from 'redux-saga/lib/internal/sagaHelpers';
import { NAMESPACE_SEP } from './constants';function getSaga(effects, model, onError, onEffect) {return function*() {for (const key in effects) {if (Object.prototype.hasOwnProperty.call(effects, key)) {const watcher = getWatcher(key, effects[key], model, onError, onEffect);const task = yield sagaEffects.fork(watcher);yield sagaEffects.fork(function*() {yield sagaEffects.cancel(task);});}}};
}function getWatcher(resolve, reject, key, _effect, model, onError, onEffect) {let effect = _effect;let type = 'takeEvery';let ms;if (Array.isArray(_effect)) {// effect 是数组而不是函数的情况下暂不考虑}function *sagaWithCatch(...args) {try {yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@start` });const ret = yield effect(...args.concat(createEffects(model)));yield sagaEffects.put({ type: `${key}${NAMESPACE_SEP}@@end` });resolve(key, ret);} catch (e) {onError(e);if (!e._dontReject) {reject(key, e);}}}const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);switch (type) {case 'watcher':return sagaWithCatch;case 'takeLatest':return function*() {yield takeLatest(key, sagaWithOnEffect);};case 'throttle':return function*() {yield throttle(ms, key, sagaWithOnEffect);};default:return function*() {yield takeEvery(key, sagaWithOnEffect);};}
}function createEffects(model) {// createEffects(model) 的逻辑
}function applyOnEffect(fns, effect, model, key) {for (const fn of fns) {effect = fn(effect, sagaEffects, model, key);}return effect;
}
import { handleActions } from 'redux-actions';
import createSagaMiddleware from 'redux-saga/lib/internal/middleware';const prefixedModel = models.map(m => {return prefixNamespace({...m});
});
const reducers = {}, sagas = [];
for (const m of prefixedModel) {reducers[m.namespace] = getReducer(m.reducers,m.state,handleActions);if (m.effects)sagas.push(getSaga(m.effects, m, onError, onEffect));
}const sagaMiddleware = createSagaMiddleware();
sagas.forEach(sagaMiddleware.run)

react-coat

在掘金上看到一篇文章与DvaJS风云对话,是DvaJS挑战者?还是又一轮子?,发现了另一个 react 状态与数据流管理框架 react-coat,以下是代码示例:

// 仅需一个类,搞定 action、dispatch、reducer、effect、loading
class ModuleHandlers extends BaseModuleHandlers {@reducerprotected putCurUser(curUser: CurUser): State {return {...this.state, curUser};}@reducerpublic putShowLoginPop(showLoginPop: boolean): State {return {...this.state, showLoginPop};}@effect("login") // 使用自定义loading状态public async login(payload: {username: string; password: string}) {const loginResult = await sessionService.api.login(payload);if (!loginResult.error) {// this.updateState()是this.dispatch(this.actions.updateState(...))的快捷this.updateState({curUser: loginResult.data});Toast.success("欢迎您回来!");} else {Toast.fail(loginResult.error.message);}}// uncatched错误会触发@@framework/ERROR,监听并发送给后台@effect(null) // 不需要loading,设置为nullprotected async ["@@framework/ERROR"](error: CustomError) {if (error.code === "401") {// dispatch Action:putShowLoginPopthis.dispatch(this.actions.putShowLoginPop(true));} else if (error.code === "301" || error.code === "302") {// dispatch Action:路由跳转this.dispatch(this.routerActions.replace(error.detail));} else {Toast.fail(error.message);await settingsService.api.reportError(error);}}// 监听自已的INIT Action,做一些异步数据请求@effect()protected async ["app/INIT"]() {const [projectConfig, curUser] = await Promise.all([settingsService.api.getSettings(),sessionService.api.getCurUser()]);// this.updateState()是this.dispatch(this.actions.updateState(...))的快捷this.updateState({projectConfig,curUser,});}
}

参考资料:

  1. Redux 中文文档
  2. React Native Training
  3. Redux 中文文档
  4. Stackflow 上关于 redux-saga 的一个回答

Redux 学习总结 (React)相关推荐

  1. 【前端学习】React学习资料

    React 是一种开源的 JavaScript 库,用于构建用户界面.它由 Facebook 开发并维护,已经成为了当今最流行的前端库之一.与其他框架不同,React 主要专注于视图层(View),旨 ...

  2. 学习使用React和Electron一次构建自己的桌面聊天应用程序

    by Alex Booker 通过亚历克斯布克 学习使用React和Electron一次构建自己的桌面聊天应用程序 (Learn to build your own desktop chat app ...

  3. react开发模式_通过开发带有精灵动画的游戏来学习高级React模式

    react开发模式 by Pavel Vlasov 通过帕维尔·弗拉索夫(Pavel Vlasov) 通过开发带有精灵动画的游戏来学习高级React模式 (Learn advanced React p ...

  4. [Redux/Mobx] 在React中你是怎么对异步方案进行选型的?

    [Redux/Mobx] 在React中你是怎么对异步方案进行选型的? 小项目使用简单的redux-thunk方案,增加的代码量极少,只有两个api,上手成本低 大项目使用基于redux-saga的d ...

  5. Redux学习(一)——Redux的使用过程

    一.为什么需要redux JavaScript开发的应用程序,已经变得越来越复杂了: JavaScript需要管理的状态越来越多,越来越复杂: 这些状态包括服务器返回的数据.缓存数据.用户操作产生的数 ...

  6. React学习(六)-React中组件的数据-state

    虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 ID:suibichuanji 点击下方 ...

  7. React学习(十)-React中编写样式CSS(styled-components)

    虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 VX-ID:suibichuanji 点 ...

  8. React学习(九)-React中发送Ajax请求以及Mock数据

    虽互不曾谋面,但希望能和你成为笔尖下的朋友 以读书,技术,生活为主,偶尔撒点鸡汤 不作,不敷衍,意在真诚吐露,用心分享 点击左上方,可关注本刊 撰文 | 川川 VX-ID:suibichuanji 点 ...

  9. node.js学习之react,redux,react-redux

    redux好难懂,终于明白了一点,做了个小demo,记录一下. 先看目录结构: src |--index.js |--actions |--index.js |--components |--Addi ...

最新文章

  1. 如何将CSS应用于iframe?
  2. C#动态属性(.NET Framework4.5支持)
  3. Maven中scope依赖范围和依赖的传递性
  4. MYSQL 去除重复 记录
  5. docker下交叉编译环境配置
  6. P1232 [NOI2013] 树的计数
  7. python时间去掉t_Python的set集合详解
  8. 判断sem信号量为零_将信号量递减为零的进程崩溃时,如何恢复信号量?
  9. arcgis字段计算器无法赋值_Arcgis空间连接工具的妙用
  10. linux nohup不生成日志,linux重定向及nohup不输出的方法
  11. 经典解释监视器和对象锁
  12. jq 实现头像(气泡式浮动)
  13. 注册双击Ctrl键 (DLL版)
  14. stc15f2k60f2单片机定时器_8 STC15F2K60S2单片机的定时器计数器 例题
  15. 分销系统开发 三级分销技术开发
  16. 【.net】C#如何发送电子邮件
  17. python for循环次数_Python for循环
  18. android 六边形简书,六边形RecyclerView
  19. 计算机专业在广东省的录取分数线,深圳大学广东本科批稳居广东高校前4!2019各省录取分数线发布!...
  20. 接受投资人投入材料一批_接受投资者投入材料的会计分录

热门文章

  1. LVS/HAProxy/Nginx负载均衡对比
  2. 【OpenCV学习】XML的读写
  3. SQL SERVER 取所有表及注释 和 字段属性
  4. vue全家桶+Koa2开发笔记(5)--nuxt
  5. 居住7年未交一分钱天然气使用费 女房主替租户偿还近4万元欠款
  6. getAttribute() 与 attr() 的区别
  7. 诺基亚没放弃 它买下这家公司,要成为电信界的扛把子
  8. CentOS各版本挂载光盘做yum源安装
  9. 嵌入式移植NTP(Network Time Protocol)
  10. 沈阳人才市场7月精品招聘会