资讯专栏INFORMATION COLUMN

Redux 源码拾遗

CloudwiseAPM / 3461人阅读

摘要:循环还没有结束,其中的某个对进行了添加或者删除,都会影响到此次循环的进行,带来不可预期的错误。

首先来一段 redux 结合 中间件 thunk、logger 的使用demo 了解一下应该如何使用

const redux = require("redux")
const { 
  createStore,
  combineReducers,
  bindActionCreators,
  compose,
  applyMiddleware
} = redux

const simpleState = { num: 0 }
const complexState = { num: 1 }

// logger中间件负责打印日志
function logger({dispatch, getState}) {
  return function(next) {
    return function(action) {
      console.log("logger start:" + action.type)
      const res = next(action)
      console.log("logger end:" + action.type)
      return res
    }
  }
}

// thunk中间件负责异步
function thunk({dispatch, getState}) {
  return function(next) {
    return function(action) {
      if (typeof action === "function") {
        return action(dispatch)
      }
      return next(action)
    }
  }
}

// 派发的增加方法
function increment(num) {
  return {
    type: "increment",
    payload: num
  }
}

// 派发的减少的方法
function decrement(num) {
  return {
    type: "decrement",
    payload: num
  }
}

// 派发的乘法的方法
function multiply(num) {
  return {
    type: "multiply",
    payload: num
  }
}

// 派发的除法的方法
function divide(num) {
  return {
    type: "divide",
    payload: num
  }
}

// 派发异步事件方法
function syncDivide() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(divide(10))
    }, 2000)
  }
}

// 负责加减法的reducer
function simpleCalcReducer(state = simpleState, action) {
  switch (action.type) {
    case "increment":
      return Object.assign({}, state, { num: state.num + action.payload })
    case "decrement":
      return Object.assign({}, state, { num: state.num - action.payload })
    default: 
      return state
  }
}

// 负责乘除法的reducer
function complexCalcReducer(state = complexState, action) {
  switch (action.type) {
    case "multiply":
      return Object.assign({}, state, { num: state.num * action.payload })
    case "divide":
      return Object.assign({}, state, { num: state.num / action.payload })
    default: 
      return state
  }
}

const middleware = applyMiddleware(thunk, logger)
const allReducers = combineReducers({
  simpleCalcReducer,
  complexCalcReducer
})
const store = createStore(allReducers, middleware)

// 所有action
const allActions = {
  increment,
  decrement,
  multiply,
  divide,
  syncDivide
}
const actions = bindActionCreators(allActions, store.dispatch)

// 派发action
actions.increment(2);
actions.decrement(1);
actions.multiply(10);
actions.divide(5);
actions.syncDivide()
setTimeout(() => {
  console.log(store.getState())
}, 2500)

打印结果如下:

上面只是简单使用了redux的部分api,来尽量实现一个标准项目中所使用到的redux的基本操作流程,下面进入源码的分析阶段

1.项目结构
第一张图,先来看一下 redux 的项目结构目录

---src
------utils // 工具函数库
---------actionTypes.js // redux用来初始化state,自身调用的action
---------isPlainObject.js // 用来检测action 是否为一个明确的对象
---------warning.js // 用来进行警告的公共方法
------applyMiddleware.js // 中间件,负责对store.dispatch方法进行包装
------bindActionCreators.js // 对派发action的语法进行优化,使之可以更好的应用于react或其他
------combineReducers.js // 将分离的reducer合并成一个对象,并返回一个执行这些reducer的函数
------compose.js // 合并函数, 将函数参数按照从左向右的顺序进行合并,返回一个新函数
------createStore.js // 核心方法,返回一个store对象,用来获取state、派发action、添加订阅者subscribe 等
------index.js // 入口文件,对外导出的方法

2.utils 工具方法的分析
将utils的三个方法放在最前面来分析,主要是为了先对三个工具方法有个简单概念和了解,这样在阅读下面的内容时,对于有用到工具方法的地方可以直接理解,不用再打断思绪去分析工具方法,影响整体的阅读体验

① warning.js -- 控制台打印异常日志

export default function warning(message) {
  if (typeof console !== "undefined" && typeof console.error === "function") {
    console.error(message)
  }
  try {
    throw new Error(message)
  } catch (e) {}
}

总结:
warining 函数非常简单,当 console.error 方法存在时来打印 错误的 message,这里的一层判断是为了兼容ie6789浏览器只有在开启控制台的情况下,console对象才会创建,否则会报console未定义而导出程序无法进行。
至于下面的为什么要 try 里面去抛出异常,本身这样做对程序是没有什么影响的,这样的意义又是什么呢?源码的注释里解释道 “This error was thrown as a convenience so that if you enable "break on all exceptions" in your console, it would pause the execution at this line ”,翻译过来就是,抛出这个错误是为了方便查看错误源。只要我们开启了断点异常,那么程序就会在抛出错误的那行代码上打上断点,来方便进行调试和追踪。那么在谷歌里面这个异常怎么开启呢?F12打开谷歌的开发者工具,1点击 Sources - 2点击蓝色的pause icon - 3勾选 Pause on caught exceptions,如下图所示

在控制台里测试如下代码

键入回车后,浏览器出现断点,跳转至sources资源文件,并高亮了抛出错误的那行代码,非常方便

② isPlainObject.js -- 判断目标是否为明确的对象

export default function isPlainObject(obj) {
  if (typeof obj !== "object" || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return Object.getPrototypeOf(obj) === proto
}

总结:
Object.getPrototypeOf 方法用来返回对象的隐式原型,即构造这个对象的构造函数的原型。
使用 while 循环来查找目标对象的原型链顶层,因为 Object.prototype.__proto__ === null,所以Object.prototype 就是原型链的顶层,查找到最后一层时,proto 一定被赋值为 Object.prototype。
这样做的意义是什么? Array数据类型 或者 dom的 document等数据类型的typeof 也是 object, 他们都不是直接由Object 函数来构建的对象,以 Array 数组的构造函数来举例。

var ary = new Array()  
var pro = Object.getPrototypeOf(ary)
console.log(pro === ary.__proto__) // true
console.log(ary.__proto__ === Array.prototype)  // true

var pro2 = Object.getPrototypeOf(pro)
console.log(pro2 === Object.prototype)  // true
Object.prototype.__proto__ === null  // true

可见 ary 第一次获取到的原型是 Array.prototype,而 Array.prototype 本身也是一个对象,必然由 Object 函数创建,所以 Array.prototype.__proto__ 又指向了 Object.prototype,到此循环结束。最终 pro = Object.prototype。这就造成了最终的 Object.getPrototypeOf(obj) 和 proto 是不等的。
所以这个方法的就很清晰了,只有直接使用 Object 函数创建的对象才会被判断为真,因为只有它原型链存在一层

③ actionTypes.js -- redux 内部做初始化state的action

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

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

export default ActionTypes

总结:
randomString() 方法随机生成一个36进制的数字,然后切割拼接,最终生成 和"5.b.p.3.b.8" 格式一样的字符串
这个方法 导出了一个对象,对象包含3个key: INIT、REPLACE、PROBE_UNKNOWN_ACTION,前两个是字符串,后面一个是方法,方法也是返回一个拼接好的字符串,其实这三个都是redux内部用来派发action 的 type

3.模块分析(去掉注释)
① 入口文件 index.js**

import createStore from "./createStore"
import combineReducers from "./combineReducers"
import bindActionCreators from "./bindActionCreators"
import applyMiddleware from "./applyMiddleware"
import compose from "./compose"
import warning from "./utils/warning"
import __DO_NOT_USE__ActionTypes from "./utils/actionTypes"

function isCrushed() {}

if (
  process.env.NODE_ENV !== "production" &&
  typeof isCrushed.name === "string" &&
  isCrushed.name !== "isCrushed"
) {
  warning(
    "You are currently using minified code outside of NODE_ENV === "production". " +
      "This means that you are running a slower development build of Redux. " +
      "You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify " +
      "or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) " +
      "to ensure you have the correct code for your production build."
  )
}

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

总结:
入口文件作用就时将几个模块文件引入和导出,里面有一个空的 isCrushed 函数,这个函数的意义就是判断当前的构建工具是否为 node 环境,如果是 node 环境并且非生产环境,那么就要判断当前的 redux 文件有没有被压缩。
判断的目的就是希望开发者,在非生产环境下不要压缩代码,如果项目比较大,我们只是修改了一个小小的样式,这时候如果开启代码的混淆压缩,那么我们项目的所有依赖的文件都会被混淆压缩,项目越大被压缩的内容越多耗时越长,从而导致调试的时间增加,降低开发效率。这也正是redux在 warning 警告里提到的 "This means that you are running a slower development build of Redux",你正在一个缓慢的开发环境下使用 redux。

② 核心文件 creteStore.js
cretaeStore.js 是redux的核心文件,在这个方法里,redux 向外提供了 dispatch、subscribe、getState、replaceReducer 这四个核心方法。此外还有一个 [$$observable] 方法,这个方法并不是很好理解他的作用和意义,放在文章最后来说明。下面是移除了注释的源代码

import $$observable from "symbol-observable"
import ActionTypes from "./utils/actionTypes"
import isPlainObject from "./utils/isPlainObject"

export default function createStore(reducer, preloadedState, enhancer) {
  // 判断1
  if (
    (typeof preloadedState === "function" && typeof enhancer === "function") ||
    (typeof enhancer === "function" && typeof arguments[3] === "function")
  ) {
    throw new Error(
      "It looks like you are passing several store enhancers to " +
        "createStore(). This is not supported. Instead, compose them " +
        "together to a single function."
    )
  }

  // 判断2
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState
    preloadedState = undefined
  }

  // 判断3
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

  // 判断4
  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
  }

  // 内部变量
  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

  function ensureCanMutateNextListeners() {
    // ...
  }

  function getState() {
    // ...
  }

  function subscribe(listener) {
    // ...
  }

  function dispatch(action) {
    // ...
  }

  function replaceReducer(nextReducer) {
    // ...
  }

  function observable() {
    // ...
  }

  dispatch({ type: ActionTypes.INIT })

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

createStore 方法接收3个参数,分别是 reducer,preloadedState,enhancer。
1.reducer 就是一个纯函数用来返回 state
2.preloadedState 是初始化的state,在实际开发中,很少有传递这个这个参数。其实这个参数就是为了初始化必须的原始数据。此外,如果使用了combineReducer这个方法来组合多个reducer,相应的preloadedState 里的key 也必须要和 combineReducer 中的key相对应
3.enhancer 翻译过来就是增强器,它是一个类似的高阶函数用来包装和增强 creteStore 内部的 dispatch、subscribe、getState 等方法,通过这个高阶函数可以实现 中间件、时间旅行、持久化state等,在redux内只实现了一个enhancer,它就是中间件 applyMIddleware,用来强化 dispatch方法。

讲完三个参数,开始解释代码

  if (
    (typeof preloadedState === "function" && typeof enhancer === "function") ||
    (typeof enhancer === "function" && typeof arguments[3] === "function")
  ) {
    throw new Error(
      "It looks like you are passing several store enhancers to " +
        "createStore(). This is not supported. Instead, compose them " +
        "together to a single function."
    )
  }

判断1:提示很明确,当使用多个enhancer时,需要使用compose 方法将多个enhancer合并成一个单一的函数。

// 判断2
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState
    preloadedState = undefined
  }

判断2:个人感觉这里的判断就是为了符合更大众化的开发需求,preloadedState 这个参数在实际开发中,真的很少传递。所以在这里做了一个判断,如果开发中没有用到这个初始的preloadedState,完全可以将它省略掉,直接传递最后一个enhancer函数,redux在内部帮开发者,完成了这部分参数转换的处理。

// 判断3
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.")
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

判断3:当enhancer 存在并且为函数的时候,直接将当前creteStore方法作为参数传递给 enhancer。而这个enhancer(createStore),返回的就是 createStore 的加强版可以称他为 creteStore-X,至于如何增强先放到后面的applyMiddleware这个enhancer来说明。这里先只需要知道,通过enhancer包装过后的 createStore,内部的某些方法被加强了。

  // 判断4
  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.")
  }

判断4:reducer 必须是一个函数 函数 函数

  let currentReducer = reducer
  let currentState = preloadedState
  let currentListeners = []
  let nextListeners = currentListeners
  let isDispatching = false

内部变量:
这里分别使用了,currentReducer 和 currentState 来接收初始传递的 reducer 和 preloadedState,重新赋值是因为 currentReducer 和 currentState 都是可变的,当他们被修改的时候不会影响初始的reducer 和 preloadedState。
currentListeners 是一个数组用来存储订阅的函数列表,为什么还要多定义一个 nextListeners = currentListeners 呢?这个问题放到后面看比较好理解,先掠过
isDispatching 比较好理解,用来判断redux是否处于派发action的状态中,即是否在执行reducer。

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

ensureCanMutateNextListeners:返回currentListeners 的浅拷贝
这里的意图不是很明显,当nextListeners 和 currentListeners 全等的时候,返回一个 currentListeners 的 浅拷贝赋值给 nextListenenrs,意义是什么呢接着向下看。

 function getState() {
    if (isDispatching) {
      throw new Error(
        "You may not call store.getState() while the reducer is executing. " +
          "The reducer has already received the state as an argument. " +
          "Pass it down from the top reducer instead of reading it from the store."
      )
    }

    return currentState
  }

getState:返回当前的currentState
返回当前的currentState,当action还在 派发当中时,如果调用这个方法会抛出错误

  function subscribe(listener) {
     // 被添加的订阅者必须是一个函数
    if (typeof listener !== "function") {
      throw new Error("Expected the listener to be a function.")
    }
    // 处于dispatch的时候,不允许添加订阅者
    if (isDispatching) {
      throw new Error(
        "You may not call store.subscribe() while the reducer is executing. " +
          "If you would like to be notified after the store has been updated, subscribe from a " +
          "component and invoke store.getState() in the callback to access the latest state. " +
          "See https://redux.js.org/api-reference/store#subscribe(listener) for more details."
      )
    }
    // 这里是一个闭包,isSubscribed 用来表示当前的订阅者已经成功的被
    // 添加到了订阅的列表中
    let isSubscribed = true
    // 当nexListeners 和 currentListeners 全等时候,返回了一个新的
    // nextListeners, 然后将订阅者添加到新的 nextListeners
    ensureCanMutateNextListeners() // ?1
    nextListeners.push(listener)
    // 返回一个 unsubscribe, 用来取消已经添加到订阅列表中的订阅者
    return function unsubscribe() {
        // 如果当前的订阅者已经被取消了 直接返回
      if (!isSubscribed) {
        return
      }
      // 处于dispatch的时候,不允许取消订阅者
      if (isDispatching) {
        throw new Error(
          "You may not unsubscribe from a store listener while the reducer is executing. " +
            "See https://redux.js.org/api-reference/store#subscribe(listener) for more details."
        )
      }
      // 将订阅者的状态改为false
      isSubscribed = false
      // 继续更新 nextListeners
      ensureCanMutateNextListeners()
      // 将取消的订阅者从 nextListeners 中删除
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

subscribe:用来添加和删除订阅者
这样粗略看下来似乎也没太看清楚这个 ensureCanMutateNextListeners 方法有什么卵用,继续翻看redux 对ensureCanMutateNextListeners 方法的解释,里面提到
"This makes a shallow copy of currentListeners so we can use nexListeners as a temporary list while dispatching"
"This prevents any bugs around consumers calling subscribe/unsubscribe in the middle of a dispatch"
主要就是后面这一句,这个方法 可以防止用户在执行dispatch中调用订阅或者取消订阅时出现任何错误。还是不明所以,继续向下寻找答案

  function dispatch(action) {
      // 判断 action 是否为明确的 object 对象
    if (!isPlainObject(action)) {
      throw new Error(
        "Actions must be plain objects. " +
          "Use custom middleware for async actions."
      )
    }
     // action 必须有 type 这个字段类型
    if (typeof action.type === "undefined") {
      throw new Error(
        "Actions may not have an undefined "type" property. " +
          "Have you misspelled a constant?"
      )
    }
      // 不允许同时执行多个 dispatch
    if (isDispatching) {
      throw new Error("Reducers may not dispatch actions.")
    }
      // 将执行状态设置为true, 开始执行reudcer, 并接收 currentState
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
        // 无论成功失败 都将执行状态重置
      isDispatching = false
    }
      // reducer 执行完毕之后,开始循环执行 订阅者数组列表
    const listeners = (currentListeners = nextListeners) // ?2
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i] // ?3
      listener()
    }

    return action
  }

dispatch:用来派发action,执行reducer
问题2,每次执行订阅者列表的时候,为什么要新定义一个 listeners 来接收 nextListeners,直接 currentListeners = nextListeners 然后使用 currentListeners 来循环不行吗?
问题3,循环里面为什么不直接使用 listeners[i]() 来执行调用,而是定义一个变量来接收然后再执行?
再结合上面 subscribe 中 ensureCanMutateNextListeners 的到底有何意义?
其实,这一切的细节都是为了解决一种特殊的应用场景,在订阅事件内部 再次 添加订阅或者取消订阅。如
store.subscribe( () => { store.subscribe(...) })
如果是这种场景,因为是在循环中去触发 listener,单纯的使用 currentListeners 来存储订阅列表是无法满足的。循环还没有结束,其中的某个 listener 对 currentListeners 进行了添加或者删除,都会影响到此次循环的进行,带来不可预期的错误。至此这些细节就变得清晰起来了。
引用官方的注释更为准确,每次dispatch后,循环使用的是当前nextListeners 的快照,同样也就是 currentListeners,它在循环结束之前是不会被改变的。想象一下,假如在订阅事件的内部继续调用 store.subsrcibe 来添加订阅者,那么就会调用 ensureCanMutateNextListeners 这个方法,如果currentListeners 和 nextListeners 是完全相等的说明nextListeners 还未被改变,此时浅拷贝一份 currentListenrs 的队列赋值为 nextListeners,nextListeners 就变成了一个全新的订阅队列,然后将 添加的订阅者放到新的 nextListeners,这样就完全不会影响到之前已经开始的循环。当下次disptach 再次发起的时候,将 currentListeners 同步为最新的 nextListeners 队列。
问题 2 应该如何理解呢?找到了早期redux的提交记录如下:

这里还没有对 currentListeners 和 nextListeners 做概念的区分,只是将每次listeners 浅拷贝了一层用来 安全的执行循环。所以 const listeners = (currentListeners = nextListeners) 中声明的 listeners并不是必须的,他的存在只是为了在之后在循环中使用 listeners 代替 currentListeners 少打几个字母而已
问题 2 应该如何理解呢?其实在这里是为了确保在 listener 当中的 this 与我们对 js 当中的函数内部 this 指向谁的预期保持一致。这里就是将 this 重新绑定为默认的全局对象,如果直接使用 listeners[i]() 来调用,那么其内部的this 变指向了listeners 这个数组本身。

  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.")
    }
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

replaceReducer:替换reducer,并且重新 dispatch
replaceReducer 这个方法比较简单,当我们的项目启用了模块懒加载,我们的最开始的reducer可能只是部分的模块的reducer,这时候如果要引入新的模块就需要能够动态的替换 reducer 来更新state。官方的注释里还写道,如果在开发模式下启用了热更新,同样需要这个函数来进行替换。

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== "object" || observer === null) {
          throw new TypeError("Expected the observer to be an object.")
        }
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }
        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },
      [$$observable]() {
        return this
      }
    }
  }

observable:此方法返回一个 observable 对象,该对象拥有 subscribe 的方法来添加一个可观测的对象。那么这个方法到底时干什么用的呢?注释里有写道 “For more information, see the observable proposal:https://github.com/tc39/propo...”,打开这个地址看到这个项目是将 可观察类型引入到ECMAScript标准库中,而redux 这里就是实现了 observable 的观察对象。这里不是很清楚如何使用 ... 略过。

  dispatch({ type: ActionTypes.INIT })

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

最后使用 dispatch({ type: ActionTypes.INIT }) 生成一个初始化的state,这也就是为什么preloadedState不需要传递,就可以得到初始化的state了。因为redux内部在执行 createStore 这个方法的时候,自动执行了一次 disptach。最后将众方法返回

③ combineReducers -- 将多个reducer 合并成一个函数

import ActionTypes from "./utils/actionTypes"
import warning from "./utils/warning"
import isPlainObject from "./utils/isPlainObject"

// 函数1
function getUndefinedStateErrorMessage(key, action) {
  // ...
}

// 函数2
function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  // ...
}

// 函数3
function assertReducerShape(reducers) {
  // ...
}

// 函数4 
export default function combineReducers(reducers) {
  // ...
}
function getUndefinedStateErrorMessage(key, action) {
  const actionType = action && action.type
  const actionDescription =
    (actionType && `action "${String(actionType)}"`) || "an action"

  return (
    `Given ${actionDescription}, reducer "${key}" returned undefined. ` +
    `To ignore an action, you must explicitly return the previous state. ` +
    `If you want this reducer to hold no value, you can return null instead of undefined.`
  )
}

getUndefinedStateErrorMessage: 方法比较简单,返回一段提示信息,提示 reducer不能返回undefined

function getUnexpectedStateShapeWarningMessage(
  inputState,
  reducers,
  action,
  unexpectedKeyCache
) {
  const reducerKeys = Object.keys(reducers)
  const argumentName =
    action && action.type === ActionTypes.INIT
      ? "preloadedState argument passed to createStore"
      : "previous state received by the reducer"
    // reducer 不存在时进行的提示
  if (reducerKeys.length === 0) {
    return (
      "Store does not have a valid reducer. Make sure the argument passed " +
      "to combineReducers is an object whose values are reducers."
    )
  }
      // state 为非明确的对象的时候的提示
  if (!isPlainObject(inputState)) {
    return (
      `The ${argumentName} has unexpected type of "` +
      {}.toString.call(inputState).match(/s([a-z|A-Z]+)/)[1] +
      `". Expected argument to be an object with the following ` +
      `keys: "${reducerKeys.join("", "")}"`
    )
  }
  // 当state当中的某个key值无法在reducers当中找到,并且还未被加入到unexpectedKeyCache
  // 那么就把这个key筛选出来
  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )
  // 将上一步骤筛选出来的key,存储到unexpectedKeyCache里面
  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })
  // 如果是使用了 replaceReducer 替换了reducer,那么就不需要进行提示了,因为之前
  // 的state 的数据可能和 新的reducer 不能保持一致
  if (action && action.type === ActionTypes.REPLACE) return
  // 将state里面reducer不能操作的key打印出来
  if (unexpectedKeys.length > 0) {
    return (
      `Unexpected ${unexpectedKeys.length > 1 ? "keys" : "key"} ` +
      `"${unexpectedKeys.join("", "")}" found in ${argumentName}. ` +
      `Expected to find one of the known reducer keys instead: ` +
      `"${reducerKeys.join("", "")}". Unexpected keys will be ignored.`
    )
  }
  }

getUnexpectedStateShapeWarningMessage: 在非生产环境对finalReducer进行的校验,将state内异常的 key 抛出,进而提示开发者。

function assertReducerShape(reducers) {
    // 循环所有reducer, 并使用 ActionTypes.INIT 和 ActionTypes.PROBE_UNKNOWN_ACTION()
    // 两个type类型的 action 校验所有reducer 是否返回了符合规范的 state
  Object.keys(reducers).forEach(key => {
    const reducer = reducers[key]
    const 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. If you don"t want to set a value for this reducer, ` +
          `you can use null instead of undefined.`
      )
    }

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION()
      }) === "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, but can be null.`
      )
    }
  })
}

assertReducerShape:用来断言reducers返回的结果是不是符合要求的类型,如果不满足判断它会抛出一个error
可以看出在不使用 combineReducers 的时候,我们编写的唯一的reducer是不用这些校验的,如果我们在其中返回了 undefined 那么必然最终的 currentState 就变成了 undefined。
那么为什么 combineReducers 这个方法强制所有的 reduer 不能返回 undefined 呢?
找到了redux的中文文档:http://cn.redux.js.org/docs/a...
里面提到:”combineReducers 函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根 redcuer 时并不需要遵守这些规则“
这样就很清楚了,原来多的这些校验是为了更好的提示新手用户,reducer的正确使用规范。
这里还有一个问题,使用 {type: ActionTypes.INIT} 来校验 reducer 是否返回正确的state,ActionTypes.INIT 应该必然是 reducer 内部未知的 action 了。但是为什么下面还要用 { type: ActionTypes.PROBE_UNKNOWN_ACTION() } 在重复校验一次吗?难道说只是为了说明两次校验的出发目的不一样?但是这样是不是多余了

export default function combineReducers(reducers) {
  // 接收一个对象形式的 reducers 集合如 { reducer1: () => {}, reducer2: () => {}, }
  const reducerKeys = Object.keys(reducers)
  // 最终将要被执行的reducers 集合
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
     // 判断当前如果是非生成环境 并且 reducers[key] 不存在时在控制台打印警告信息
    if (process.env.NODE_ENV !== "production") {
      if (typeof reducers[key] === "undefined") {
        warning(`No reducer provided for key "${key}"`)
      }
    }
     // 只有当 reducers[key] 为函数类型时 才添加到 finalReducers 当中
    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key]
    }
  }
  // 获取finalReducers 当中的 key
  const finalReducerKeys = Object.keys(finalReducers)
  // 用来存放state中不能被操作的 key
  let unexpectedKeyCache
  if (process.env.NODE_ENV !== "production") {
      // 为了优化性能只在非生产模式下进行校验
    unexpectedKeyCache = {}
  }
  // 用来校验finalReducers中每一个reducer是否合乎规范
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
  return function combination(state = {}, action) {
      // 到这里开始执行 reducer
    if (shapeAssertionError) {
        // 如果reducer返回了undefined直接抛出错误
      throw shapeAssertionError
    }
    // 非生产环境进行提示
    if (process.env.NODE_ENV !== "production") {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    // 判断 state 有没用被更改
    let hasChanged = false
    // 重新生成的state
    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)
      if (typeof nextStateForKey === "undefined") {
          // 如果reducer 返回了undefined 抛出错误
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 如果state 内的某个key的数据已经被更改过 此处必然是 true
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 根据state是否改动过 返回对应的state
    return hasChanged ? nextState : state
  }
}

combineReducers:这个方法就是将多个reducers循环执行后最终返回一个合并后的state,这个方法还是比较简单的。

④-- bindActionCreators,提供了一种调用dispatch的其他方式,代码本身比较简单,这里就不在赘述了

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== "object" || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? "null" : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }

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

⑤ -- compose, 组合函数,将多个函数参数按照传入的顺序组合成一个函数,并返回。第二个参数作为第一个函数的参数,第三个参数作为第二个函数的参数,依次类推。返回的函数接收的参数,将作为compose 初始参数的最后一个函数的参数,源码如下

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

  if (funcs.length === 1) {
    return funcs[0]
  }
   // 最终 compose 返回的是一个 (...args) => a(b(...args)) 这样的函数
  // 那么这个函数内部调用的顺序也就清楚了,执行这个函数的时候,首先会执行 
  // var c = b(...args),  然后将c 作为参数传递给 a,如果多个函数的话依次类推,
  // 最先执行最里面的那个参数,由里向外
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

问题1,先执行了b(...args)函数,不就直接开始从里面执行了吗?demo1

var funcs2 = [
  function a() {
    console.log("a")
  },
  function b() {
    console.log("b")
  },
  function c(arg) {
    console.log(arg)
    console.log("c")
  }
]
var t = compose(funcs2)
t("d")
// d c b a

说好的剥洋葱呢,怎么就成半个了?
其实compose这个方法只是提供了组合函数的功能,真正想要实现完整的从外到内这种完整的洋葱模型,还需要对传递到compose参数的每个函数做处理,它的每个函数一定是返回了一个新的函数,这样才能确保它不会被简单的执行,思考下面的demo

var funcs = [
  function a(b) {
    return () => {
      console.log("a")
      return b();
    }
  },
  function b(c) {
    return () => {
      console.log("b");
      return c();
    }
  },
  function c(arg) {
    return () => {
      console.log("c");
      console.log(arg)
    }
  }
]
var f = compose(funcs)("d");
f()
// a b c d

这就和文章最开始实现的 logger 和 thunk 的中间件很相似了。a 里执行 b , 然后 b 里执行 c,一个中间件的雏形已经出现了。

⑥ -- applyMiddleware,redux内唯一实现的enhancer,用来扩展dispatch方法,也是redux中最难理解的地方,一睹真容吧,为了便于对执行过程的理解,这里贴一下中间件redux-thunk源码的简化版

({ dispatch, getState }) => next => action => {
  if (typeof action === "function") {
    return action(dispatch, getState);
  }
  return next(action);
}
import compose from "./compose"
export default function applyMiddleware(...middlewares) {
   // 这里 applyMiddleware 返回的函数接收的第一个参数是 creteStore,
   // 这就是之前 createStore内直接使用 enhancer(createStore)(reducer, preloadedState)
   // 的原因了。 
  return createStore => (...args) => {
      // store 就是最原始的 store 对象,这里还未对store.dispatch 进行扩展
    const store = createStore(...args)
      // 声明一个dispatch的方法,注意这里使用的时let,表示这个dispatch将要会被改变
      // 而改变过后的dispatch 就是加入了中间价增强版的 dispatch
    let dispatch = () => {
      throw new Error(
        "Dispatching while constructing your middleware is not allowed. " +
          "Other middleware would not be applied to this dispatch."
      )
    }
    // middlewareAPI和字面意思一样就是,中间件需要用到的原始的store对象的方法,,
    // 这里提供了两个方法,一个getState, 一个是dispatch。等等这里好像有点不一样的东西
    // 为什么这里 要写成 dispatch: (...args) => dispatch(...args),而不是直接
    // dispatch: dispatch  先留下继续向下看。
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 这里参考上面的redux-thunk的源码 chain最终返回的结果就是
    /*
       chain = [ next => action => {
          if (typeof action === "function") {
            return action(dispatch, getState);
            // 此处的dispatch 和 getState 即为 middlewareAPI的dispatch 和 getState
          }
          return next(action);
        } ]
        */
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 结合之前已经分析过的compose,compose(...chain)最后一个参数来接收 store.dispatch
    // 因为这里只传递了一个thunk中间件,所以,这里的 dispatch 就变成了
    /* 
        dispatch = (action) => {
          if (typeof action === "function") {
            return action(dispatch, getState);
            // 此处的dispatch 和 getState 依然为 middlewareAPI的dispatch 和 getState
          }
          // 由于next被赋值为store.dispatch, 此处实际为
          return store.dispatch(action);
        }
    */
    // 也因为这里dispatch 被重新赋值,导致middlewareAPI内dispatch属性
    // 里使用到的 dispatch 也变了,不再是抛出error错误的那个函数了。
    // 这就是为什么一定要写成 dispatch: (...args) => dispatch(...args) 的原因了
    // 如果写成 dispatch: dispatch, 相当于只是初始声明了这个方法,后续dispatch的修改就与它无关了
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
    // 返回的dispatch 替换掉了 store 内部初始的 dispatch, dispatch被扩展了
  }
}

总结:
通过对源码的翻阅,了解到了整个redux的执行流程和机制。通过createStore来初始化state,当需要使用异步action的时候,可以使用 applyMiddleware 将redux-thunk redux-saga 等中间件对store.dispatch 进行扩展。每次调用 dispatch 都会执行所有的reducer,reducer执行完毕后,会更新所有的订阅事件。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54838.html

相关文章

  • Redux 源码拾遗

    摘要:循环还没有结束,其中的某个对进行了添加或者删除,都会影响到此次循环的进行,带来不可预期的错误。 首先来一段 redux 结合 中间件 thunk、logger 的使用demo 了解一下应该如何使用 const redux = require(redux) const { createStore, combineReducers, bindActionCreators, ...

    zhangfaliang 评论0 收藏0
  • 【underscore 源码解读】Array Functions 相关源码拾遗 & 小结

    摘要:最近开始看源码,并将源码解读放在了我的计划中。将转为数组同时去掉第一个元素之后便可以调用方法总结数组的扩展方法就解读到这里了,相关源码可以参考这部分。放个预告,下一篇会暂缓下,讲下相关的东西,敬请期待。 Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。 阅读一些著名框架类库的源码,就好...

    SimpleTriangle 评论0 收藏0
  • 【underscore 源码解读】Object Functions 相关源码拾遗 & 小结

    摘要:直接来看例子一目了然,第一个参数是对象,第二个参数可以是一系列的值,也可以是数组数组中含,也可以是迭代函数,我们根据值,或者迭代函数来过滤中的键值对,返回新的对象副本。 Why underscore 最近开始看 underscore.js 源码,并将 underscore.js 源码解读 放在了我的 2016 计划中。 阅读一些著名框架类库的源码,就好像和一个个大师对话,你会学到很多。...

    neuSnail 评论0 收藏0

发表评论

0条评论

CloudwiseAPM

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<