Lemonreds / snippets

code snippets.
https://github.com/Lemonreds/snippets/issues
2 stars 0 forks source link

[2018-08-26]: 阅读Redux源码02 - createStrore #6

Open Lemonreds opened 6 years ago

Lemonreds commented 6 years ago

Redux的核心功能基本都在 createStrore 中,我们使用Redux也由此方法开始,可以说是最为重要的一个方法,下面我们就来了解一下createStore究竟是怎么工作的。

createStore 方法预览

方法的签名

首先来看 createStore 的方法签名:

function createStore(reducer, preloadedState, enhancer) 

方法接收三个参数:

方法的返回

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

返回一个Store对象,对象暴露以上的方法给外部:

方法的作用

创建并返回一个维护State树的Store对象,更新State树的唯一方法是调用 Store.dispatch(),一个Redux应用应该只有一个Store,如果需要模块化state树的处理逻辑,则可以编写多个reducer,使用Redux另一个API (combineReducers,后续会说到)合并成一个 reducer 作为参数传入 createStore。

详细解读 CreateStore

接下来看看详细代码:

方法的开头首先对传入的参数进行了类型判断:

  /**
   *  这里使得 createStore 可以忽略参数 preloadedState 
   *  从而可以这样调用: createStore(reducer,enhancer)
   */
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  /**
   *  检查 enhancer 是否是函数
   *  主要是应用于 Redux 中间件
   */
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    /** 
    * 暂且跳过这里  applyMiddleware 时再进行讲解
    * 可以暂时这样认为:
    * enhancer(createStore)(reducer, preloadedState) 这一系列函数执行完后
    * 返回的也是一个Store对象 只不过这个对象被加工了 
    */ 
    return enhancer(createStore)(reducer, preloadedState)
  }
  /**
   * 检查 reducer 是否是函数
   */
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

接着初始化一些变量:

  // 当前的 reducer 函数
  // 主要用于 Store.replaceReducer() 替换 reducer 时使用
  let currentReducer = reducer

  // currentState 就是 store 中的状态树,这里先对它进行了初始化
  // Store.getState() 返回此变量
  let currentState = preloadedState

  // 以下的两个变量主要用于 Store.subscribe()方法

  // 当前的订阅者
  let currentListeners = []
  // 下一次 Store.dispatch() 更新state树 需要发出通知的订阅者
  let nextListeners = currentListeners

  // 是否正在调用 Store.dispatch() 方法 
  // Store.dispatch方法调用时,会禁止一些操作执行
  let isDispatching = false

接下来一个个看看Store暴露的接口方法:

Store.getState()

Store.getState() 是最简单的一个方法,直接返回当前的状态树,也就是 currentState 变量。

  /**
   * 返回当前的状态树 state 
   * 不允许在调用 Store.dispatch() 方法时获取状态树
   */
  function getState() {
    if (isDispatching) {
      throw new Error(
        'xxxxxx'
      )
    }
    return currentState
  }

Store.dispatch()

Store.dispatch()方法,传入一个action对象,更新state树的唯一途径就是调用此方法。

调用例子:

Store.dispatch({
    type: "ADD_TODO",
    text: "Read the source code."
})

同样,函数的开始先对入参合法性进行判断(因为JavaScript是弱类型语言- -、):

function dispatch(action) {
    // 检查 action 是否为纯对象
    // 纯对象即是通过 {} 或者 new Object() 创建的对象,其原型为 Object.prototype
    if (!isPlainObject(action)) {
      throw new Error(
        'xxxxxx'
      )
    }
    // 检查 action.type 是否存在
    // type 字段是必须的 
    if (typeof action.type === 'undefined') {
      throw new Error(
        'xxxxx'
      )
    }
    // 同一时刻只能执行一个 dispatch 函数
    // 为了防止在 dispatch 函数嵌套调用 dispatch 函数
    if (isDispatching) {
      throw new Error('xxxx')
    }

入参类型都正确后,接着将当前的 state树 和传入的 action 传递给 reducer 执行 ,如何去更新state树的数据处理逻辑是由reducer决定的,reducer 执行相关的数据处理逻辑,返回新的state树,从而更新state树:

    try {
      // 设置变量
      // 正在更新状态树 一些操作将被禁止
      isDispatching = true
      // 传入 state,action 交由 reducer 函数 执行 
      // reducer 返回新的 state
      currentState = currentReducer(currentState, action)
    } finally {
      // 状态更新完成 设置为false
      isDispatching = false
    }

由于state树的更新,需要向订阅监听的函数发出通知:

/* 
* 更新了state树 向所有的订阅者发出通知
* 任何时刻 增加一个监听器时(调用Store.subscribe()) 首先会将监听器加到 nextListeners 数组中
* 直到下一个 dispatch 发起执行时 才会同步 currentListeners 和 nextListeners
* 然后对最新的订阅数组发出通知 执行所有订阅函数
*/
// 同步最新的订阅数组
const listeners = (currentListeners = nextListeners)
// 执行所有的监听函数
for (let i = 0; i < listeners.length; i++) {
  const listener = listeners[i]
  listener()
}

函数的最后,返回了入参action:

    // 返回了原有的入参 action
    // 在应用redux中间件时 此返回将发挥极大的作用
    return action

这个返回其实对于调用者而言,并没有多大用处。它真正发挥作用的时候是在于扩展Redux中间件的时候,也就是applyMiddleware方法,后续会进一步解读。

dispatch方法完成的任务主要有三个:

  1. 检查入参action是否合法
  2. 将当前state树和入参action传递给reducer方法,由reducer计算新的state后,更新state树。
  3. 更新了state树,向所有订阅者发出通知,也就是执行所有监听函数。

Store.subscribe()

接下来看Store.subscribe(),方法的作用是订阅state树的变化,每次执行完dispatch方法后,都会触发监听,执行传入的监听函数。

调用例子:

// 注册一个监听函数 当state树更新时 输出最新的state值
Store.subscribe(()=>{
    console.log(Store.getState())
})

在对subscribe方法解读之前,先看看方法内部用到的一个函数 ensureCanMutateNextLisenteners。

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      // slice() 不传入参数的话 就是拷贝整个数组,返回一个新的数组
      nextListeners = currentListeners.slice()
    }
  }

函数十分简单,判断当前订阅和下次订阅是否指向同一个数组,如果是的话就拷贝数组,使得 nextListeners 和 currentListneres 保存两份数据一样的数组。但是为什么要这样做呢?首先应该明确的是,不管是注册监听,还是注销监听,都只会在 nextListeners 上操作,直到 dispatch 被调用时,才会同步 currentListneres 和 nextListeners ,也就是 dispatch 中的 :

const listeners = (currentListeners = nextListeners)

dispatch完成后 currentListeners 和 nextListeners 这两个变量就会指向同一个数组,在此之后,如果你注册注销监听,在 nextListeners 上操作的同时,势必也会影响到 currentListeners 这个变量,这样就混淆了 currentListeners 和 nextListeners 两个变量的作用,所以需要一个 ensureCanMutateNextLisenteners 函数,保证在nextListeners 上注册注销监听,都不会影响到 currentListeners 。具体调用场景看下面对subscribe方法的解读:

同样,subscribe 方法一开始也是对入参 listener 进行判断:

    // 检查参数类型是否正确
    if (typeof listener !== 'function') {
      throw new Error('xxxx')
    }
    // 不允许 dispatch 函数正在执行的时候进行订阅
    if (isDispatching) {
      throw new Error(
        'xxxx'
      )
    }

接着用闭包保存了一个变量,用来标记监听函数是否正在监听

let isSubscribed = true

因为要对 nextListeners 进行操作,所以调用了 ensureCanMutateNextLisenteners ,确保操作不会影响到 currentListeners

    // 确认修改 nextListeners 时 不影响到 currentListeners
    ensureCanMutateNextListeners()
    // 将新的监听函数加入到下一次监听函数执行队列
    nextListeners.push(listener)

函数的最后,返回一个函数,用来注销监听:

    // 返回一个取消监听的函数
    return function unsubscribe() {
      if (!isSubscribed) {
        // 当然 取消订阅只能取消一次...
        return
      }
      // 不允许在 dispatch 的时候取消监听函数
      if (isDispatching) {
        throw new Error(
          'xxxxxxxxxxxx'
        )
      }
      // 注销监听
      isSubscribed = false
      // 确认修改 nextListeners 时 不影响到 currentListeners
      ensureCanMutateNextListeners()
      // 删除这个监听函数,下一次 dispatch 时会生效
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }

subscribe 方法完成的任务只有两个:

  1. 注册监听
  2. 返回一个注销监听的函数

Store.replaceReducer()

接下来看看同样简单的 Store.replaceReducer() 方法:

  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('xxxxx')
    }
    // 更新reducer
    currentReducer = nextReducer
    // 会触发一个Redux独有的action,来确定状态树是否能响应新的reducer变化
    dispatch({
      type: ActionTypes.REPLACE
    })
  }

方法的作用就是替换reducer函数,达到热更新reducer的效果,一般很少用到。值得注意的是,方法之中 dispatch 了一个 action:

dispatch({
      type: ActionTypes.REPLACE
})

可见,Redux在实现内部自己也会触发一些action,具体的作用来看看 ActionTypes 这个常量的定义。

解读 ActionTypes.js

ActionTypes 来自  ./src/utils/actionTypes.js ,源码如下:

const randomString = () =>
  Math.random()
    .toString(36)
    .substring(7)
    .split('')
    .join('.')

const ActionTypes = {
  INIT: `@@redux/INIT${randomString()}`, // str
  REPLACE: `@@redux/REPLACE${randomString()}`, // str
  PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` // fun
}

export default ActionTypes

就是通过 randomString 生成三个任意的,不可预测的 action.type 名称,这些 action 仅供 Redux内部使用。例如上面使用到了 REPLACE 这个action,整个createStore代码的最后部分,可看到还发起了一个 type为 INIT 的 action :

  // 偷偷发起了一个action
  dispatch({
    type: ActionTypes.INIT
  })
  // 然后就是正常的导出:
  return {
      //....
  }

为什么Redux需要使用到这些私有action呢? 其实非常简单,原因在于Redux对reducer计算新的state树的时候有严格的要求,为了严格约束reducer,需要触发一定的action来检测返回的新state树是否符合预期,具体要求是这样的:

  1. 对于任何未知的操作,reducer需要返回当前状态
  2. 如果当前状态未定义,即传入的state为undefined,则reducer需要返回初始状态

也就是以下两种情况:

  1. dispatch( undefined , action ) 传入的初始state为undefined时,reducer需要返回一个初始state值。
  2. dispatch( crruentState , unkownAction ) 传入一个未知action时,reducer必须返回原有的state。

第一点是也为初始化state树,使得不做任何更新之前,都能通过getState()访问到state树。 第二点中,如果 dispatch 了一个未知的action,reducer什么也没有返回,即函数默认返回了undefined,则在 dispatch 在更新state树的时候:

  currentState = currentReducer(currentState, action)

就会设置当前状态 currentState = 'undefined' ,导致丢失state的值。

最后

createStore源码不难阅读,最重要的是要理解state,action,reducer等概念的作用。方法返回的Store对象基本包含了我们使用Redux的方法。但其中没有包含更多细节,比如我们要如何使用多个Reducer划分数据处理逻辑,如何应用中间件等,这些操作还要看Redux提供的其他方法。