React 全家桶之 redux

redux 流程图

redux 的使用

先看一下目录结构

components/counter.js:创建store

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'
import reducers from '../reducers';
const store = createStore(reducers, applyMiddleware(thunk))

export default store;

reducers/counter.js:构建组件初始状态;创建纯函数reducer;接收action,返回全新的state。

const initialState = {
    num: 0
}

const reducer = (state = initialState, action) => {
    const { type, data } = action;
    switch (type) {
        case 'increment':
            return {
                num: state.num + data
            }
        case 'decrement':
            return {
                num: state.num - data
            }
        default:
            return state;
    }
}

export default reducer;

reducers/index.js:组合多个 reducer,通过 action.type 将 action 分发到具体的reducer中处理

import { combineReducers } from 'redux';
import counter from './counter';
export default combineReducers({
    counter
})

actions/counter.js:actionCreator,产出 action 的地方;返回值为一个拥有 type 字段的 object对象,或者是一个函数(需要加入redux-thunk 中间件予以支持,后面会讲到)

 import { bindActionCreators } from 'redux';
 import store from '../store';

 const actionCreator = {
    increment: () => {
        return {
            type: 'increment',
            data: 1
        }
    },
    decrement: () => {
        return (dispatch, getState) => {
            setTimeout(() => {
                dispatch({
                    type: 'decrement',
                    data: 2
                })
            }, 1000)
        }
    },
    reset: () => {
        return {
            type: 'reset'
        }
    }
}

actionCreator.reset = bindActionCreators(actionCreator.reset, store.dispatch);

export default actionCreator

components/counter.js:view层,执行 dispatch -> 触发 action -> action 经过相应的 reducer 处理后,返回全新的 state -> 执行 store.subscribe 注册的监听函数 -> 监听函数中 setState 更新状态 -> 触发 react 更新机制 -> 组件更新

import React from 'react';
import store from '../store';
import actionCreator from '../actions/counter'

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: store.getState().counter.num
    }
    store.subscribe(() => {
      const state = store.getState();
      this.setState({
        num: state.counter.num
      })
    })
  }
  increment = () => {
    store.dispatch(actionCreator.increment())
  }
  decrement = () => {
    store.dispatch(actionCreator.decrement())
  }
  reset = () => {
    actionCreator.reset()
  }
  render() {
    const { num } = this.state;
    return (
      <div className="App">
        {
          num
        }
        <div></div>
        <button onClick={ this.increment }>+</button>
        <button onClick={ this.decrement }>-</button>
        <button onClick={ this.reset }>reset</button>
      </div>
    );
  }

}

export default Counter;

redux 原理解析

整个过程用到了几个重要的方法:

  • createStore:创建 store
  • store.dispatch:触发 action
  • store.subscribe:注册监听函数,dispatch中会调用所有注册的监听函数
  • store.getState:返回当前 state
  • combineReducers:组合多个 reducer 成一个,根据 action.type 分发 action 到对应的 reducer 中处理
  • bindActionCreators:用 dispatch 封装 actionCreator,这样在组件中直接调用 actionCreator 方法就能执行 dispatch,而不需要在组件中显示的调用 dispatch
  • applyMiddleware:利用中间件,增强 dispatch 的功能

下面分别大致说一下每个方法的实现过程

createStore

createStore 接收三个参数 (reducer, preloadedState, enhancer),reducer 好理解,preloadedState 是初始状态,很少用到,enhancer 是增强器,用来增强 redux。

上面的代码中我是这样写的 createStore(reducers, applyMiddleware(thunk)),enhancer写在了第二个参数上,第三个参数没有传,之所以能生效是因为下面这个判断

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
    preloadedState = undefined
  }

常见的增强器有 redux-thunk、redux-saga,需要使用 applyMiddleWare 处理之后才是合格的 enhancer。

该函数内部定义了这么几个重要的变量

let currentState = preloadedState //从函数createStore第二个参数preloadedState获得
let currentReducer = reducer  //从函数createStore第一个参数reducer获得
let currentListeners = [] //当前订阅者列表
let nextListeners = currentListeners //新的订阅者列表
let isDispatching = false

createStore 执行完会返回这么几个东西:

{
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
 }

dispatch

if (!isPlainObject(action)) {
  // dispatch 的入参必须是一个简单的 Object 对象
  throw new Error(
    'Actions must be plain objects. ' +
      'Use custom middleware for async actions.'
  )
}

if (typeof action.type === 'undefined') {
  // 并且这个 Object 需要具有 type 属性
  throw new Error(
    'Actions may not have an undefined "type" property. ' +
      'Have you misspelled a constant?'
  )
}

if (isDispatching) {
  throw new Error('Reducers may not dispatch actions.')
}

try {
  isDispatching = true  // 可以理解为线程锁,防止两个action同时执行
  currentState = currentReducer(currentState, action) // 得到最新的state
} finally {
  isDispatching = false
}

const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()    // 执行所有的监听函数
}

subscribe

nextListeners.push(listener)

// 返回解绑函数
return function unsubscribe() {
  ...
  const index = nextListeners.indexOf(listener)
  nextListeners.splice(index, 1)
}

getState

return currentState as S

combineReducers

它的作用是用来合并多个 reducer,最终会返回一个函数代替所有的 reducer。在 dispatch 后,这个合并后的 combination 会遍历所有的 reducer 并执行它们,拿到最新的 state。然后与原来的 state 对比看是否是同一个引用,如果是就返回老的 state,否则返回新的 state。

先将入参 reducers 拷贝到 finalReducers 中
const finalReducerKeys = Object.keys(finalReducers)
return function combination(state = {}, action) { 
    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
        const key = finalReducerKeys[i]
        const reducer = finalReducers[key]
        const previousStateForKey = state[key]
        const nextStateForKey = reducer(previousStateForKey, action)

    nextState[key] = nextStateForKey
    hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state
}

bindActionCreators

经过该方法封装后的 actionCreator 具备了 dispatch 能力,所以 actionCreator 直接调用就可以触发 action,不用在组件中显示的调用 dispatch。如 actions/counter.js 中的 reset,在组件中直接调用 counter.reset() 就可以触发 dispatch,而不用这么写 store.dispatch(counter.reset())

function bindActionCreator<A extends AnyAction = AnyAction>(
  actionCreator: ActionCreator<A>,
  dispatch: Dispatch
) {
  return function(this: any, ...args: any[]) {
    return dispatch(actionCreator.apply(this, args))
  }
}
export default function bindActionCreators(
  actionCreators: ActionCreator<any> | ActionCreatorsMapObject,
  dispatch: Dispatch
) {
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  /***********容错**********/

  const boundActionCreators: ActionCreatorsMapObject = {}
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

applyMiddleware

在 dispatch 函数中我们提到,它的入参必须是一个简单的 Object 对象,而且还需要有 type 属性。如 actions/counter.js 中的 increment,它的返回值就是带有 type 字段的 Object 对象。

而 decrement 这个 actionCreator 的返回值却是一个函数,最后也顺利的触发了 action,更新了试图。这是为什么呢?如果依据 dispatch 的源码来看,这肯定会报错才对啊?

原因就是我们使用了 redux-thunk 这个中间件增强了 dispatch 的功能,使其具备了接收函数作为入参的能力。

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
    }
  }
}
  •  增强器 enhancer 是 applyMiddleware 方法的返回值,这个 enhancer 主要分为一下几步:
  • 入参是 createStore,通过它创建一个 store
  • 定义 middlewareAPI,所有的中间件都需要实现 getState、dispatch 这两个方法
  • middlewares 调用 Array.prototype.map 进行改造,存放在 chain
  • 用compose整合chain数组,并赋值给dispatch
  • 将新的dispatch替换原先的store.dispatch

这样看还有有些懵逼,我们来看一下大名鼎鼎的 redux-thunk 是怎么实现的。看懂了它再回头去看 applyMiddleware 就豁然开朗了

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

我们最终得到的 thunk 应该是这个样子的

const thunk = ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
};

它的入参是一个简单 Object,且拥有 dispatch 和 getState 两个属性。我们现在再把这个 thunk 带回到 applyMiddleware 方法中的 chain 那一步

const chain = middlewares.map(middleware => middleware(middlewareAPI))

执行 thunk 可以得到如下结果,是一个函数,它的入参是 next。返回值也是一个函数,入参是 action。为了方便理解,我们假设使用了两个中间件

// thunk(middlewareAPI)
const result1 = next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
};
const result2 = 同上
// 那么上一步的 chain 就是这个样子
chain = [result1, result2]

现在我们得到了 chain 的模样,接着把 chain 带入到 applyMiddleware 中

dispatch = compose(...chain)(store.dispatch)

compose 函数是用来组合多个函数的,把一个函数的执行结果作为下一个函数的入参。我们来看下源码

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)))
}

那么 chain 经过 compose 处理之后得到的应该是一个函数

const composeRst = (args) => result1(result2(args))

走到这里,我们再回头看 result1 的代码,就能够知道它的入参 next 其实就是下一个中间件。我们接着往下执行

dispatch = compose(...chain)(store.dispatch)
等价于
dispatch = composeRst(store.dispatch)
等价于
dispatch = result1(result2(store.dispatch))
等价于
dispatch = action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return result2(action);
}

也就是说经过 compose(...chain)(store.dispatch) 一顿操作猛如虎,最终得到的其实就是一个函数,这个函数的入参是一个 action。这不就是 dispatch 的功能吗!!!

但是比原始的 dispatch 多了个 if 判断,action 原本只能是一个带有 type 属性的简单 Object,现在可以传 function 了,然后把 dispatch、getState 传入了这个 function。

利用这一点我们就可以在 actionCreator 中返回一个异步函数去后端获取数据,然后在异步函数的回调中调用 dispatch 更新状态,如文章开头 actions/counter.js 中的 decrement 这个 actionCreator,是不是觉得很巧妙。鼎鼎大名的 redux-thunk 其实就那么几行代码。

整个 redux 的使用及原理讲完了,但是代码这么写一点也不够优雅。通常在 react 项目中使用 redux,我们会借用 react-redux 做为桥梁,更加优雅的使用 redux。下一篇讲解 react-redux 的使用和原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章