博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Redux 学习总结 (React)
阅读量:6654 次
发布时间:2019-06-25

本文共 18964 字,大约阅读时间需要 63 分钟。

在 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 创建函数 来自动 dispatchconst boundAddTodo = text => dispatch(addTodo(text))boundAddTodo(text)

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

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 creator      REMOVE: [        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 });// Mapconst 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.filter    default:      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 (      
    {todos.map(todo => (
    ))}
); }}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(  
, 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

通过使用指定的 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

    dispatch(actions.loadUserProfile(123)}>Robert
    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

    dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert
    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.props    dispatch({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 actionsfunction* 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});   }}// orfunction 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" gets  dispatched while a fetch is already pending, that pending fetch is cancelled  and 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 middlewareconst sagaMiddleware = createSagaMiddleware()// mount it on the Storeconst store = createStore(  reducer,  applyMiddleware(sagaMiddleware))// then run the sagasagaMiddleware.run(mySaga)// render the application

路由跳转

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

dva 框架

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

在掘金上看到一篇文章,发现了另一个 react 状态与数据流管理框架 ,以下是代码示例:

// 仅需一个类,搞定 action、dispatch、reducer、effect、loadingclass ModuleHandlers extends BaseModuleHandlers {  @reducer  protected putCurUser(curUser: CurUser): State {    return {...this.state, curUser};  }  @reducer  public 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,设置为null  protected async ["@@framework/ERROR"](error: CustomError) {    if (error.code === "401") {      // dispatch Action:putShowLoginPop      this.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,    });  }}

参考资料:

转载地址:http://jfnto.baihongyu.com/

你可能感兴趣的文章
centos7 搭建SVN环境
查看>>
Cookie
查看>>
第一周总结
查看>>
一分钟完成MySQL5.7安装部署
查看>>
ORA-10458、ORA-01196和ORA-01110错误解决办法
查看>>
Python和Ruby哪个好学?python基础
查看>>
为什么喜欢自驾游?
查看>>
爱创课堂每日一题九十三天-html常见兼容性问题?
查看>>
2.10 环境变量PATH 2.11 cp命令 2.12 mv命令 2.13 文档查看cat/mor
查看>>
OSI模型
查看>>
Java之品优购课程讲义_day14(7)
查看>>
大快DKhadoop大数据处理平台详解
查看>>
如何制作出像顶尖咨询公司咨询顾问们那种风格的 PPT?
查看>>
共享经济影响电商 华亚优品商城凭“共创共享”模式受捧
查看>>
Swarm群集搭建
查看>>
shell
查看>>
造成HashMap非线程安全的原因
查看>>
嵌入式Linux内核学习路线和学习linux内核的建议
查看>>
Esxi更新补丁
查看>>
ldap理论属于概念缩略词
查看>>