redux 源码解析

467 阅读8分钟
原文链接: github.com

周末两天时间,把redux(3.6.0)和react-redux(4.4.5)源码注释了一遍。注释文件分别在redux-docreact-redux-doc中。

redux源码很简单,react-redux有些复杂,但只要花点心思仔细阅读,你会发现,还是那么难懂。

废话不多说,这篇文章详解redux源码。看源码之前一定要熟读KPI,不用倒背如流但一定要清楚不同传参形式与柯里化调用(其实看源码之前看一遍API就行了)。

核心概念

redux的核心思想来自于flux,可以说flux是规范,而state是实现。flux 的提出主要是针对现有前端 MVC 框架的局限总结出来的一套基于 dispatcher 的前端应用架构模式。如果用 MVC 的命名习惯,它应该叫 ADSV(Action Dispatcher Store View)。正如其名,Flux 的核心思想就是数据和逻辑永远单向流动。

redux 参考了 flux 的设计,但是对 flux 许多冗余的部分(如 dispatcher)做了简化,同时将函数式编程的思想融合其中。

  • state: 通过store.getState函数返回,当前store的数据状态。
  • store: 通过createStore函数创建,又包含了下面4个核心方法。
    • getState():获取 store 中当前的状态。
    • dispatch(action):分发一个 action,并返回这个action,这是唯一能改变 store 中数据的方式。
    • subscribe(listener):注册一个监听者,它在 store 发生变化时被调用。
    • replaceReducer(nextReducer):更新当前 store 里的 reducer,一般只会在开发模式中调用该方法。
  • reducer: reducer是一个处理函数,用来处理每个动作的函数集合。
  • action: action 是一个普通的 JavaScript 对象,一般包含 type、payload 等字段,用于描述一个事件以及需要改变的相关数据。
  • middleware: 它提供了一个分类处理 action 的机会。在 middleware 中,你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会。

createStore

createStore函数用来创建store,而store是redux的核心,几乎redux所有概念都跟store有关。

首先我们看看createStore的函数签名:

function createStore(reducer, preloadedState, enhancer) {
  // ...
}

我们在使用createStore时候,通常只传递reducer,剩下的两个参数是3.1.0以后才加入的。preloadedState是初始化state值。enhancer是上文提到的middleware,这个思想来自于koa-middleware。

我们看看接下来是如何工作的

  // 如果初始化传入的是函数并且没有传入中间件,则初始化函数设为中间件
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }

这种接口设计模式类似于jquery,即不定向函数签名。

接下来声明了一些列变量

  // currentState: 当前数据
  var currentReducer = reducer                      // 当前reducer
  var currentState = preloadedState                 // 初始化状态
  var currentListeners = []                         // 当前订阅的事件
  var nextListeners = currentListeners              // 下次订阅的事件
  var isDispatching = false                         // 是否正在分配事件

这里为什么要区分当前订阅事件与下次订阅事件呢?后面我们会给出答案

  /* 获得当前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)
    }
  }

getState没什么好说的。订阅函数也很清楚明了,订阅一系列要执行的方法,返回一个函数,再次执行取消订阅。这与原生js的设计完全不同。原生js的removeEventListener需要指定原绑定函数。

ensureCanMutateNextListeners函数会把当前订阅的函数拷贝给下次订阅的函数,为什么这么做,带上刚才的问题,一会再讲(现在真的没法讲,讲了你也听不懂。)

下面是store中最重要的的函数dispatch,整个redux的数据变化都是靠它完成,包括一部分界面render,我们看下dispatch的源码

  /* 发布函数 */
  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.')
    }

    // 执行当前reducer,当前reducer可以被replace掉
    try {
      // 执行的过程中,记录状态,如果执行的过程中再执行,会报错
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 每次发布时候,更新当前订阅函数。发布的时候再订阅,会把下次订阅的函数从当前函数拷贝一份,防止当前执行发布函数受影响
    var listeners = currentListeners = nextListeners

    // 依次执行每个订阅函数
    for (var i = 0; i < listeners.length; i++) {
      var listener = listeners[i]
      listener()
    }

    return action
  }

我们可以清晰的看到这里把当前的状态和action传递给reducer,并且把返回结果重新赋值给currentState。用isDispatching来控制执行发布中不能再次发布。

这里解释一下刚才的疑问,为什么每次订阅时候都放在nextListeners中。

发布的过程中,会依次执行所有订阅函数,如果发布的过程中再次订阅,那么当前发布时所执行的订阅数组也会有影响。

redux如何解决这个问题呢?所以每次发布过程中,拿到的是订阅的一个副本(currentListeners=nextListeners),而再次订阅(或者取消订阅)时,会拿这个副本拷贝给订阅的函数( nextListeners = currentListeners.slice()。也就是之前说过的ensureCanMutateNextListeners函数。)

接下来替换掉reducer函数。源码清晰可见,不做过多解释。

  // 替换掉当前 reducer
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    // 替换后,重新执行init函数
    dispatch({ type: ActionTypes.INIT })
  }

最后是 observable 函数,配合其他库使用。

  function observable() {
    // subscribe 就是上面的那个 subscribe订阅函数
    var outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object') {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          // 调用 observer 的 next 方法,获取当前 state。
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        // 将 observeState 当作一个 listener,dispatch 之后自动调用 observer 的 next 方法。
        var unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [?observable]() {
        return this
      }
    }
  }

observable 是为了配合 Rxjs 这样 observable/reactive 库,不久的将来 EMCAScript 可能会支持原生的 observable/reactive 对象。tc39/proposal-observable

最后执行一下初始化

dispatch({ type: ActionTypes.INIT })

bindActionCreateors

这个函数可以把dispatch(action)动作直接作为属性传递给react组件,而不用在组件内部再次dispatch(action),这么做的好处是做到react与redux完全分离,组件内部无感知redux的存在。

拿todo list举例:

// actions.js
function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}

const actions = { addTodo, removeTodo }

// App.js
class App extends Component {
  render() {
    const { visibleTodos, visibilityFilter, actions } = this.props
    return (
      <div>
        <AddTodo
          onAddClick={text =>
            actions.addTodo(text)
          }/>
        <TodoList
          todos={visibleTodos}
          onTodoClick={index =>
            actions.completeTodo(index)
          }/>
        <Footer
          filter={visibilityFilter}
          onFilterChange={nextFilter =>
            actions.setVisibilityFilter(nextFilter)
          }/>
      </div>
    )
  }
}

function mapDispatchToProps(dispatch, a) {
  return { actions: bindActionCreators(actions, dispatch) }
}

const FinalApp = connect(select, mapDispatchToProps)(App)

ReactDOM.render(
  <Provider store={createStore(reducer)}>
    <FinalApp />
  </Provider>,
  document.getElementById('app')
)

这样就可以把dispatch包装后的actions直接传递给app,而app内部无需再diapatch(action)

源码实现也非常简单

function bindActionCreator(actionCreator, dispatch) {
  // 包装一层dispatch,返回高阶函数,因为原来的actionCreator是可以传递参数的函数,包装后不能破坏原结构
  return (...args) => dispatch(actionCreator(...args))
}

export default function bindActionCreators(actionCreators, dispatch) {
  // 拿到 actionName: addTodo removeTodo
  var keys = Object.keys(actionCreators)
  var boundActionCreators = {}
  for (var i = 0; i < keys.length; i++) {
    // 第一次执行key = addTodo(string)
    var key = keys[i]
    // actionCreator = addTodo(function)
    var actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      // 包装函数:在原函数上增加dispatch包装。
      // 我们直接把经过dispatch包装过函数传递给子组件,而不是让子组件拿到传递过去dispatch再去执行,这样子组件只通过传递过去的函数调用即可,完全不知道有redux的存在
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

combineReducers

这个函数有两个用途

  • 把多个reducers合并成一个,既做到reducer解构
  • 按照解构的名称,把不同的数据分发到不同的state键下。

举例来说,我们有两个reducer,现在要合成一个。

function r1(state, action) {}
function r2(state, action) {}

const reducer = combineReducers({
  r1,
  r2
})

这样store就有两个键:r1和r2,分别存储每个reducer的返回结果。 也就是说,使用combineReducers把全局store按命名空间进行隔离。隔离的方式就是reducer的名字: { r1: state, r2: state }。

combineReducers实现有些繁琐,不过也不难。

function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers)
  // 内部创建保存reducers的变量
  var finalReducers = {}

  // 保存reducer到finalReducers中,也做了数据过滤
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i]

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

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 获得所有key, 也就是[ 'r1', 'r2' ]
  var finalReducerKeys = Object.keys(finalReducers)

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

  // 检验每个reducer是否有返回值
  var sanityError
  try {
    assertReducerSanity(finalReducers)
  } catch (e) {
    sanityError = e
  }

  return function combination(state = {}, action) {
    if (sanityError) {
      throw sanityError
    }

    if (NODE_ENV !== 'production') {
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache)
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    var hasChanged = false
    var nextState = {}
    for (var i = 0; i < finalReducerKeys.length; i++) {
      // 每个reducer的key, r1
      var key = finalReducerKeys[i]
      // 每个reducer的值, r1函数
      var reducer = finalReducers[key]
      // key的上一次状态,也就是state['r1']
      var previousStateForKey = state[key]
      // 执行每个reducer,把上一次的状态和action传入,返回一个新的状态
      var nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        var errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      // 下次的状态更新
      nextState[key] = nextStateForKey
      // 判断是否变化
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 如果有变化,返回变化后的状态
    return hasChanged ? nextState : state
  }
}

看注释并不难理解,在dispatch时候,依次执行每个reducer,因为每个reducer都可以对action.type做处理。取得上次的状态值,执行reducer并且更新数据。

以上就是对redux所有源代码进行的分析,下一篇文章会针对react-redux做代码解析。