读Redux源码02 - createStrore

栏目: 服务器 · 发布时间: 6年前

内容简介:Redux的核心功能基本都在 createStrore 中,我们使用Redux也由此方法开始,可以说是最为重要的一个方法,下面我们就来了解一下createStore究竟是怎么工作的。首先来看 createStore 的方法签名:方法接收三个参数:

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

createStore 方法预览

方法的签名

首先来看 createStore 的方法签名:

function createStore(reducer, preloadedState, enhancer) 
复制代码

方法接收三个参数:

  • reducer (func) 纯函数,接收当前状态树 state 和 发起的 action 两个参数,reducer处理完后返回一个新的state。
  • preloadedState (any) state树的初始值,虽然不限制类型,但一般使用对象来储存状态。
  • enhancer (func) 函数,用来增强store的第三方扩展,也就是中间件。

方法的返回

return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
复制代码

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

  • 通过Store.getState() 方法使读取state树
  • 通过Store.dispatch() 方法更新state树
  • 通过Store.subscribe() 方法监听state树,subscribe方法还会返回一个unsubscribe()函数用来注销监听器
  • 通过Store.replaceReducer() 方法来替换reducer。

方法的作用

创建并返回一个维护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提供的其他方法。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Zero to One

Zero to One

Peter Thiel、Blake Masters / Crown Business / 2014-9-16 / USD 27.00

“This book delivers completely new and refreshing ideas on how to create value in the world.” - Mark Zuckerberg, CEO of Facebook “Peter Thiel has built multiple breakthrough companies, and ......一起来看看 《Zero to One》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器