3

redux源码解析

什么是redux

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

为什么需要使用redux

提供了和双向绑定思想不同的单向数据流,应用状态可以预测,可以回溯,易于调试。使用redux之初的人可能会很不适应,改变一个状态,至少写三个方法,从这点上不如写其他框架代码易于理解,但是自从配合使用redux-logger一类的logger插件,就感觉到了redux的优势。状态改变很清晰,很容易了解发生了什么。

源码解析

注意: 如果没有使用过redux,建议先去看看redux文档

api方法

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose
}

可以看到我们在react代码中使用到的api,一般主动调用的就是 combineReducers ,其他部分参照例子基本可以搬过来

combineReducers

打开combineReducers.js,先看export的方法,也就是combineReducers方法

  var reducerKeys = Object.keys(reducers)
  var finalReducers = {}
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }

首先看到这个函数接收的是一个对象,而这个这个对象的内部数据值必须是一个函数,不然会警告。循环了一遍这个对象,得到一个新值,对象值全部是函数的一个新reducers

  var finalReducerKeys = Object.keys(finalReducers)

  if (process.env.NODE_ENV !== 'production') {
    var unexpectedKeyCache = {}
  }

  var sanityError
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

这里好像还在判断这个最后reducers的合法性,那这里是在判断什么呢?我们来看看 assertReducerSanity 方法

function assertReducerSanity(reducers) {
  Object.keys(reducers).forEach(key => {
    var reducer = reducers[key]
    var initialState = reducer(undefined, { type: ActionTypes.INIT })

    if (typeof initialState === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined during initialization. ` +
        `If the state passed to the reducer is undefined, you must ` +
        `explicitly return the initial state. The initial state may ` +
        `not be undefined.`
      )
    }

    var type = '@@redux/PROBE_UNKNOWN_ACTION_' + Math.random().toString(36).substring(7).split('').join('.')
    if (typeof reducer(undefined, { type }) === 'undefined') {
      throw new Error(
        `Reducer "${key}" returned undefined when probed with a random type. ` +
        `Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
        `namespace. They are considered private. Instead, you must return the ` +
        `current state for any unknown actions, unless it is undefined, ` +
        `in which case you must return the initial state, regardless of the ` +
        `action type. The initial state may not be undefined.`
      )
    }
  })
}

这块其实就两个判断,reducer被执行了两次,一个是判断没有初始化state的,reducer的返回值,一个判断action没有type的时候的返回值。一个没有返回值都会有警告,所以我们写reducer的时候都会指定一个默认返回值。

reducer会被执行多次,这也是我们为什么要保证reducer的纯粹性,不能做任何其他的操作的原因

继续往下看 combineReducers

可以看到返回了一个函数 combination(state = {}, action) 。为什么返回函数呢?

那我们看 combination(state = {}, action) 像什么?不就是我们经常写的reducer嘛!这个reducer最终会被store传入初始state并且当作纯函数调用,而reducer里面是可以嵌套combineReducers的结果的,所以我们在使用状态的时候,经常会这样 state.user.login 这样子的类似状态调用

这块想明白还是有点复杂,所有的reducer都是一个相同的函数combination,接收state参数,内部执行同样是combination,直到没有combineReducers为止,才开始执行我们自己写的reducer函数,得到的值使用combineReducers参数的对象的key作为state的key,我们自己写的reducers执行结果得到的值作为state的value。最终得到的就是一个巨大的Object,这就是我们的store中的state。

createStore

一般这个方法我们可以直接从demo中复制过来,不需要太过了解,但是既然要深入了解redux,必然要掌握这个方法

跟之前一样,先找到 export createStore 方法,可以看到这个函数接受三个参数

export default function createStore(reducer, preloadedState, enhancer) {

第一个reducer: 上文讲到的combineReducer返回的reducer函数

第二个preloadedState:redux初始化state,可以不传

第三个enhancer:中间件

  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }

    return enhancer(createStore)(reducer, preloadedState)
  }

  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

可以看到第一个判断的意思是当没有第二个参数是函数的时候,默认第二个参数就是中间件,并且默认state置为undefined

第二个判断的意思是当有中间件参数,但是中间参数类型不是function的时候,抛出一个非法错误,如果是函数,先执行中间件,退出。后续在讲中间件是怎么执行的

第三个判断reducer是否是函数,否则抛出错误退出

  var currentReducer = reducer         // 当前reducer
  var currentState = preloadedState    // 当前state
  var currentListeners = []            // 当前监听器
  var nextListeners = currentListeners // 下一个监听器
  var isDispatching = false            // 重复dispatch的状态标记

再看看createStore的返回值

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }

这不是store的方法嘛,挨个看看

  function getState() {
    return currentState
  }

这个没什么好说的。

  function subscribe(listener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected listener to be a function.')
    }

    var isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      var index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

发布订阅模式,熟悉事件系统的应该比较明白,注册一个方法而已,结果返回一个取消监听方法

  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
        'Use custom middleware for async actions.'
      )
    }

    if (typeof action.type === 'undefined') {
      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
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    var listeners = currentListeners = nextListeners
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]()
    }

    return action
  }

老几样啊,先做一些判断,我们写代码的时候好像没这么严谨哈。执行reducer,触发所有listeners。这个比较简单。

这样子,看起来createStore没什么复杂的,复杂的在哪呢?我们掠过的中间件退出的环节。所以来烧脑吧,看看中间件

想想我们创建store的时候是怎么操作的

  const finalCreateStore = compose(
    applyMiddleware(thunk, logger)
  )(createStore)

  const store = finalCreateStore(rootReducer, initialState)

这种堆在一起的代码不是太好看,分开,分开

const middlewares = applyMiddleware(thunk, logger)
const composeResult = compose(middlewares)
const finalCreateStore = composeResult(createStore)
const store = finalCreateStore(rootReducer, initialState)

这就条理清晰多了,看代码一定要看懂流程,按照顺序看,不然一头雾水,先看第一步 applyMiddleware

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

可以看到这个方法返回一个函数,既然这个函数没有被执行到,我们就先不看,现在我们得到了一个 applyMiddleware 返回的函数了

接着看 compose 方法了

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

代码更少,可是redux精髓全在这了。

compose 执行接收参数,如果参数个数是1,直接执行,上文的 applyMiddleware 的执行结果返回值是一个函数middlewares,作为参数的话,长度确实是1,所以直接返回了middlewares,也就是composeResult,所以这块是不需要compose的。而这个参数函数接收一个参数就是createStore,刚好接收createStore方法,所以我们还是进入到 applyMiddleware 的返回函数里面看看

显然 composeResult 接收到 createStore之后返回一个函数: finalCreateStore,从代码中可以看出也是可以接收中间件方法的,不过应该不会有人再在这里重复添加中间件了。

进入到 finalCreateStore 中看看

  • 创建了store,前文已经讲过了

  • 把所有的middlewares执行一遍,从这里可以看出middlewares是一个接收 { dispatch, getState } 参数的函数,不可能有其他情况

  • 把middlewares执行的结果数组作为参数再一次传入了compose

再次进入到 compose 中看逻辑,如果只有一个中间件的话,同样是把中间件直接返回,如果超过一个执行下面的逻辑

  const last = funcs[funcs.length - 1]
  const rest = funcs.slice(0, -1)
  return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))

compose 同样只是返回了一个函数。这个函数接收的参数在 applyMiddleware 里面能看到接收到的是dispatch方法

这里巧妙的利用了js Array的reduce方法,reduce方法的原理就是回调函数的返回值作为后一个回调函数的第一个参数,第一个回调函数的第一个参数的值是 reduce方法的第二个参数值。

args就是dispatch方法,这里看的出中间件函数还得返回函数,这个函数得接收类似dispatch方法的函数

看看redux-chunk这个中间件的实现吧

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;

看到 next 方法,使用过express的同学应该会很熟悉,这个next和express的next很像,原理也类似。

每个中间件的最后一层函数都是一个next,才可以在reduce里面作为参数传递,才可以实现中间件的传递

这也是redux名称的由来。

redux代码短小精悍,设计精巧,真好。


frontoldman
4.5k 声望1.3k 粉丝

前端开发者