Redux原理流程及异步请求处理

1. 对 Redux 的理解,主要解决什么问题

React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的 JavaScript 应用工具。随着 JavaScript 单页面应用(SPA)开发日趋复杂,JavaScript 需要管理的更多的状态(state),Redux 就是降低管理难度的。

Redux 支持 React、Angular、Vue、jQuery 甚至纯 JavaScript。

在 React 中,UI 以组件的形式来创建和更新,组件之间可以嵌套。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。

当项目越来越大时,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 变得非常困难。如果一个 model 的变化引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另外一个 model 的变化,依次类推,可能会影响另一个 view 的变化。直至你弄不清楚到底发生了什么。state 在什么时候,因为什么原因,如何变化依然不受控制。

当系统变得错综复杂的时候,想要重现问题或添加新的功能,就会变得举步维艰。如果这还不够复杂,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。

Redux 提供了一个叫 store 的统一仓库,组件通过 dispatch 将 state 直接传入 store,不用通过其他的组件。并且组件通过 subscribe 从 store 获取到 state 的改变。使用了 Redux ,所有的组件都可以从 store 中获取到所需的 state,他们也能从 store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。

主要解决的问题:单纯的 Redux 只是一个状态机,是没有 UI 呈现的, react-redux 作用是将 Redux 的状态机和 React UI 呈现绑定在一起,当你 dispatch action 改变 state 的时候,会自动更新页面

2. Redux 的原理及工作流程

(1) 原理: Redux 源码主要分为以下几个模块文件

  • compose.js: 提供从右到左的函数式编程
  • createStore.js: 提供生成唯一 store 的函数
  • comebineReducer.js: 提供合并多个 reducer 的函数,保证 store 的唯一性
  • bindActionCreator.js: 可以让开发者在不直接接触 dispatch 的前提下进行更改 state 的操作
  • applyMiddleware.js: 这个方法可以通过中间件来增强 dispatch 功能
const actionTypes = {
    ADD: 'ADD',
    CHANGEINFO: 'CHANGEINFO',
}
const initialState = {
    info: '初始化',
}
export default function initialReducer(state = initialState, action) {
    switch (action.type) {
        case actionTypes.CHANGEINFO:
            return {
                ...state,
                info: action.payload.info || '',
            }
            default:
                return {
                    ...state
                }
    }
}
export default function createStore(reducer, initialState, middleFunc) {
    if (initialState && typeof initialState === "function") {
        middleFunc = initialState
        initialState = undefined
    }
    let currentState = initialState
    const listeners = []
    if (middleFunc && typeof middleFunc === "function") {
        // 封装 dispatch
        reuturn middleFunc(createStore)(reducer, initialState)
    }

    const getState = () => currentState
    const dispatch = action => {
        currentState = reducer(currentState, action)
        listeners.forEach(listener => {
            listener()
        })
    }
    const subscribe = listener => {
        listeners.push(listener)
    }
    return {
        getState,
        dispatch,
        subscribe
    }
}

(2)工作流程:

  • const store = createStore(fn) 生成数据
  • action:{ type: Symbol(‘action01’), payload:’payload’} 定义行为
  • dispatch 发起 action: store.dispatch(doSomething(‘action01’))
  • reducer: 处理 action,返回新的 state

通俗点解释:

  • 首先,用户通过视图(View)发出 Action,发出方式就用到 dispatch 方法
  • 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action,Reducer 会返回新的 State
  • State 一旦变化,Store 就会调用监听函数,来更新视图 View。

以 store 为核心,可以把它看成数据存储中心,但是它不能直接进行数据的修改和更新,数据修改和更新的角色由 Reducers 来承担。store 只做存储,属于中间人的角色。当 Reducers 的更新完成以后会通过 store 的订阅来通知 react 组件,组件重新获取新的状态并渲染。

组件也能主动发送 action,创建 action 后这个动作是不会执行的,所以要 dispatch 这个 action,让 store 通过 reducers 去更新 react 里的每一个组件。

3. Redux 中异步的请求怎么处理

可以在 componentDidmount 中直接进行请求无需借助 redux。但是在一定规模的项目中,上述方法很难进行异步流的管理。通常情况下,我们会借助 redux 的异步中间件进行异步处理。 redux 异步中间件其实有很多,当下主流的异步中间件有两种 redux-thunk、redux-saga。
(1) 使用 redux-thunk 中间件
优点:

  • 体积⼩: redux-thunk 的实现⽅式很简单, 只有不到 20 ⾏代码
  • 使⽤简单: redux-thunk 没有引⼊像 redux-saga 或者 redux-observable 额外的范式, 上⼿简单
    缺点:
  • 样板代码过多: 与 redux 本身⼀样, 通常⼀个请求需要⼤量的代码, ⽽且很多都是重复性质的
  • 耦合严重: 异步操作与 redux 的 action 偶合在⼀起, 不⽅便管理
  • 功能贫乏: 有⼀些实际开发中常⽤的功能需要⾃⼰进⾏封装
    使用:
  • 配置中间件,在 store 中创建
import {
    createStore,
    applyMiddleware,
    compose
} from 'redux'
import reducer from 'reducer'
import thunk from 'redux-thunk'
// 设置调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose
// 设置中间件
const enhancer = composeEnhancers(applyMiddleware(thunk))
const store = createStore(reducer, enhancer)
export store
  • 添加⼀个返回函数的 actionCreator,将异步请求逻辑放在⾥⾯
/**
 * 发送get请求,并⽣成相应action,更新store的函数
 * @param url {string} 请求地址
 * @param func {function} 真正需要⽣成的action对应的actionCreator
 * @return {function}
 */
// dispatch为⾃动接收的store.dispatch函数
export const getHttpAction = (url, func) => (dispatch) => {
    axios.get(url).then((res) => {
        const action = func(res.data)
        dispatch(action)
    })
}
  • ⽣成 action,并发送 action
componentDidMount() {
    const action = getHttpAction('/getData', getInitTodoItemAction)
    // 发送函数类型的action时,该action的函数体会⾃动执⾏
    store.dispatch(action)
}

(2) 使⽤ redux-saga 中间件
优点:

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action 摆脱 thunk function: dispatch 的参数依然是⼀个纯粹的 action (FSA),⽽不是充满 “⿊魔法”
    thunk function
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强⼤: redux-saga 提供了⼤量的 Saga 辅助函数和 Effect 创建器供开发者使⽤, 开发者⽆须封装或者简单封装即可使⽤
  • 灵活: redux-saga 可以将多个 Saga 可以串⾏/并⾏组合起来, 形成⼀个⾮常实⽤的异步 flow
  • 易测试: 提供了各种 case 的测试⽅案,包括 mock task,分⽀覆盖等等

缺点:

  • 学习成本较高:redux-saga 不仅在使⽤难以理解的 generator function, ⽽且有数⼗个 API, 学习成本远超 redux-thunk,最重要的是你的额外学习成本是只服务于这个库的, 与 redux-observable 不同, redux-observable 虽然也有额外学习成本但是背后是 rxjs 和⼀整套思想。
  • 体积庞大:体积略⼤, 代码近 2000 ⾏,min 版 25KB 左右
  • 功能过剩:实际上并发控制等功能很难⽤到, 但是我们依然需要引⼊这些代码
  • ts ⽀持不友好: yield ⽆法返回 TS 类型

redux-saga 可以捕获 action,然后执行一个函数,那么可以把异步代码放在这个函数中,使⽤步骤如下:

  • 配置中间件
import {
    createStore,
    applyMiddleware,
    compose
} from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'
import TodoListSaga from './sagas'

// 设置调试工具
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose
const sagaMiddleware = createSagaMiddleware()

// 设置中间件
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware))

const store = createStore(reducer, enhancer)
sagaMiddleware.run(TodoListSaga)

export store
  • 将异步请求放在 saga.js 中
import {
    takeEvery,
    put
} from 'redux-saga/effects'
import {
    initTodoList
} from './actionCreator'
import {
    GET_INIT_ITEM
} from './actionTypes'
import axios from 'axios'

function* func() {
    try {
        // 可以获取异步返回数据
        const res = yield axios.get('/getData')
        const action = initTodoList(res, data)
        // 将 action 发送到 reducer
        yield put(action)
    } catch (e) {
        console.log('网络请求失败')
    }
}

function* mySaga() {
    // ⾃动捕获GET_INIT_ITEM类型的action,并执⾏func
    yield takeEvery(GET_INIT_ITEM, func)
}
export default mySaga
  • 发送 action
componentDidMount() {
    const action = getInitTodoItemAction()
    store.dispatch(action)
}