资讯专栏INFORMATION COLUMN

我的源码阅读之路:redux源码剖析

CloudwiseAPM / 1179人阅读

摘要:到月底了,小明的爸爸的单位发了工资总计块大洋,拿到工资之后第一件的事情就是上交,毫无疑问的,除非小明爸爸不要命了。当小明的爸爸收到这个通知之后,心的一块大石头也就放下来了。下面我们正式开始我们的源码阅读之旅。

前言

用过react的小伙伴对redux其实并不陌生,基本大多数的React应用用到它。一般大家用redux的时候基本都不会多带带去使用它,而是配合react-redux一起去使用。刚学习redux的时候很容易弄混淆redux和react-redux,以为他俩是同一个东西。其实不然,redux是javascript应用程序的可预测状态容器,而react-redux则是用来连接这个状态容器与react组件。可能前端新人对这两者还是觉得很抽象,打个比方说,在一个普通家庭中,妈妈在家里都是至高无上的地位,掌握家中经济大权,家里的经济流水都要经过你的妈妈,而你的爸爸则负责从外面赚钱然后交给你的妈妈。这里把你的妈妈类比成redux,而你的爸爸可以类比成react-redux,而外面的大千世界则是react组件。相信这样的类比,大家对这react和react-redux的有了一个初步认识。本篇文章介绍的主要内容是对redux的源码的分析,react-redux的源码分析将会在我的下一篇文章中,敬请期待!各位小伙们如果觉得写的不错的话,麻烦多多点赞收藏关注哦!

redux的使用

在讲redux的源码之前,我们先回顾一下redux是如何使用的,然后我们再对照着redux的使用去阅读源码,这样大家的印象可能会更加深刻点。先贴上一段demo代码:

const initialState={
  cash:200,

}
const reducer=(state=initialState,action)=>{
  const {type,payload} = action;
  switch(type){
    case "INCREMENT":
      return Object.assign({},state,{
        cash:state.cash+payload
      });
    case "DECREMENT":
      return Object.assign({},state,{
        cash:state.cash-payload
      });
    default :
      return state;
  }
}

const reducers=Redux.combineReducers({treasury:reducer});

//创建小金库
const store=Redux.createStore(reducers);

//当小金库的现金发生变化时,打印当前的金额
store.subscribe(()=>{
  console.log(`余额:${store.getState().treasury.cash}`);
});

//小明爸爸发了工资300块上交
store.dispatch({
  type:"INCREMENT",
  payload:300
});
//小明拿着水电费单交100块水电费
store.dispatch({
  type:"DECREMENT",
  payload:100
});

上面这段代码是一个非常典型的redux的使用,跟大家平时在项目里用的不太一样,可能有些小伙伴们不能理解,其实react-redux只不过在这种使用方法上做了一层封装。等当我们弄清楚redux的使用,再去看react-redux源码便会明白了我们在项目里为何是那种写法而不是这种写法。

说到redux的使用,不免要说一下action、reducer和store三者的关系。记得当初第一次使用redux的时候,一直分不清这三者的关系,感觉这三个很抽象很玄学,相信不少小伙伴们跟我一样遇到过同样的情况。其实并不难,我还是用文章开头打的比方还解释这三者的关系。

现在保险箱(store)里存放200块大洋。到月底了,小明的爸爸的单位发了工资总计300块大洋,拿到工资之后第一件的事情就是上交,毫无疑问的,除非小明爸爸不要命了。小明的爸爸可以直接将这300块大洋放到家里的保险箱里面吗?显然是不可以的,所以小明的爸爸得向小明的爸爸提交申请,而这个申请也就是我们所说的action。这个申请(action)包括操作类型和对应的东西,申请类型就是存钱(INCREMENT),对应的东西就是300块大洋(payload)。此时小明的妈妈拿到这个申请之后,将根据这个申请执行对应的操作,这里就是往保险箱里的现金里放300块大洋进去,此时小明的妈妈干的事情就是reducer干的事情。当300块大洋放完之后,小明的妈妈就通知家里的所有人现在的小金库的金额已经发生了变化,现在的余额是500块。当小明的爸爸收到这个通知之后,心的一块大石头也就放下来了。过了一会,小明回来了,并且拿着一张价值100块的水电费的催收单。于是,小明想小明妈妈申请交水电费,小明妈妈从保险库中取出来100块给了小明,并通知了家里所有人小金库的金额又发生了变化,现在余额400块。

通过上面的例子,相信小伙们对三者的关系有了一个比较清晰的认识。现在我们已经理清楚了action、reducer和store三者的关系,并且也知道了redux是如何使用的了,现在将开始我们得源码阅读之旅。

redux项目结构

本篇文章是基于redux的4.0.0版本做的源码分析,小伙伴们在对照源码的时候,千万别弄错了。整个redux项目的源码的阅读我们只需要关注src的目录即可。

这里主要分为两大块,一块为自定义的工具库,另一块则是redux的逻辑代码。先从哪块开始阅读呢?我个人建议先阅读自定义的工具库这块。主要有这么两个原因:第一个,这块代码比较简单,容易理解,大家更能进入阅读的状态;第二个,redux逻辑代码会用到这些自定义工具,先搞懂这些,对后续逻辑代码的阅读做了一个很好的铺垫。下面我们正式开始我们的源码阅读之旅。

utils actionTypes.js
const ActionTypes = {
  INIT:
    "@@redux/INIT" +
    Math.random()
      .toString(36)
      .substring(7)
      .split("")
      .join("."),
  REPLACE:
    "@@redux/REPLACE" +
    Math.random()
      .toString(36)
      .substring(7)
      .split("")
      .join(".")
}

export default ActionTypes

这段代码很好理解,就是对外暴露两个action类型,没什么难点。但是我这里想介绍的是Number.prototype.toString方法,估计应该有不少小伙伴们不知道toString是可以传参的,toString接收一个参数radix,代表数字的基数,也就是我们所说的2进制、10进制、16进制等等。radix的取值范围也很容易得出来,最小进制就是我们得二进制,所以redix>=2。0-9(10个数字)+a-z(26个英文字母)总共36个,所以redix<=36。总结一下2<=radix<=36,默认是10。基于这个特性我们可以写一个获取指定长度的随机字符串的长度:

//获取指定长度的随机字符串
function randomString(length){
  let str="";
  while(length>0){
    const fragment= Math.random().toString(36).substring(2);
    if(length>fragment.length){
      str+=fragment;
      length-=fragment.length;
    }else{
      str+=fragment.substring(0,length);
      length=0;
    }
  }
  return str;
}
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
}

isPlainObject.js也很简单,仅仅只是向外暴露了一个用于判断是否简单对象的函数。什么简单对象?应该有一些小伙伴不理解,所谓的简单对象就是该对象的__proto__等于Object.prototype,用一句通俗易懂的话就是:

凡不是new Object()或者字面量的方式构建出来的对象都不是简单对象

下面看一个例子:

class Fruit{
  sayName(){
    console.log(this.name)
  }
}

class Apple extends Fruit{
  constructor(){
    super();
    this.name="苹果"
  }
}

const apple = new Apple();
const fruit = new Fruit();
const cherry = new Object({
  name:"樱桃"
});
const banana = {
  name:"香蕉"
};

console.log(isPlainObject(apple));//false
console.log(isPlainObject(fruit));//false
console.log(isPlainObject(cherry));//true
console.log(isPlainObject(banana));//true

这里可能会有人不理解isPlainObject(fruit)===false,如果对这个不能理解的话,自己后面要补习一下原型链的相关知识,这里fruit.__proto__.__proto__才等价于Object.prototype。

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) {} 
}

这个也很简单,仅仅是打印一下错误信息。不过这里它的console居然加了一层判断,我查阅了一下发现console其实是有兼容性问题,ie8及其以下都是不支持console的。哎,不仅感叹一句!

如果说马赛克阻碍了人类文明的进程,那ie便是阻碍了前端技术的发展。
逻辑代码

到这里我已经完成对utils下的js分析,很简单,并没有大家想象的那么难。仅仅从这几个简单的js中,就牵引出好几个我们平时不太关注的知识点。如果我们不读这些源码,这些容易被忽视的知识点就很难被捡起来,这也是为什么很多大佬建议阅读源码的原因。我个人认为,阅读源码,理解原理是次要的。学习大佬的代码风格、一些解决思路以及对自己知识盲点的点亮更为重要。废话不多说,开始我们下一个部分的代码阅读,下面的部分就是整个redux的核心部分。

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 DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) " +
      "to ensure you have the correct code for your production build."
  )
}

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

index.js是整个redux的入口文件,尾部的export出来的方法是不是都很熟悉,每个方法对应了一个js,这也是后面我们要分析的。这个有两个点需要讲一下:

第一个,__DO_NOT_USE__ActionTypes。 这个很陌生,平时在项目里面我们是不太会用到的,redux的官方文档也没有提到这个,如果你不看源码你可能就不知道这个东西的存在。这个干嘛的呢?我们一点一点往上找,找到这么一行代码:

import __DO_NOT_USE__ActionTypes from "./utils/actionTypes"

这个引入的js不就是我们之前分析的utils的其中一员吗?里面定义了redux自带的action的类型,从这个变量的命名来看,这是帮助开发者检查不要使用redux自带的action的类型,以防出现错误。

第二个,函数isCrushed。 这里面定义了一个函数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 DefinePlugin for webpack (http://stackoverflow.com/questions/30030031) " +
      "to ensure you have the correct code for your production build."
  )
}

看到process.env.NODE_ENV,这里就要跟我们打包时用的环境变量联系起来。当process.env.NODE_ENV==="production"这句话直接不成立,所以warning也就不会执行;当process.env.NODE_ENV!=="production",比如是我们的开发环境,我们不压缩代码的时候typeof isCrushed.name === "string" && isCrushed.name !== "isCrushed"也不会成立;当process.env.NODE_ENV!=="production",同样是我们的开发环境,我们进行了代码压缩,此时isCrushed.name === "string" && isCrushed.name !== "isCrushed"就成立了,可能有人不理解isCrushed函数不是在的吗?为啥这句话就不成立了呢?其实很好理解,了解过代码压缩的原理的人都知道,函数isCrushed的函数名将会被一个字母所替代,这里我们举个例子,我将redux项目的在development环境下进行了一次压缩打包。代码做了这么一层转换:

未压缩

function isCrushed() {}
if (
  process.env.NODE_ENV !== "production" &&
  typeof isCrushed.name === "string" &&
  isCrushed.name !== "isCrushed"
)

压缩后

function d(){}"string"==typeof d.name&&"isCrushed"!==d.name

此时判断条件就成立了,错误信息就会打印出来。这个主要作用就是防止开发者在开发环境下对代码进行压缩。开发环境下压缩代码,不仅让我们

createStore.js

函数createStore接受三个参数(reducer、preloadedState、enhancer),reducer和enhancer我们用的比较多,preloadedState用的比较少。第一个reducer很好理解,这里就不过多解释了,第二个preloadedState,它代表着初始状态,我们平时在项目里也很少用到它,主要说一下enhancer,中文名叫增强器,顾名思义就是来增强redux的,它的类型的是Function,createStore.js里有这么一行代码:

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

    return enhancer(createStore)(reducer, preloadedState)
  }

这行代码展示了enhancer的调用过程,根据这个调用过程我们可以推导出enhancer的函数体的架子应该是这样子的:

 function enhancer(createStore) {
    return (reducer,preloadedState) => {
         //逻辑代码
        .......
    }
 }

常见的enhancer就是redux-thunk以及redux-saga,一般都会配合applyMiddleware一起使用,而applyMiddleware的作用就是将这些enhancer格式化成符合redux要求的enhancer。具体applyMiddleware实现,下面我们将会讲到。我们先看redux-thunk的使用的例子:

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers/index";

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

看完上面的代码,可能会有人有这么一个疑问“createStore函数第二个参数不是preloadedState吗?这样不会报错吗?” 首先肯定不会报错,毕竟官方给的例子,不然写个错误的例子也太大跌眼镜了吧!redux肯定是做了这么一层转换,我在createStore.js找到了这么一行代码:

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

当第二个参数preloadedState的类型是Function的时候,并且第三个参数enhancer未定义的时候,此时preloadedState将会被赋值给enhancer,preloadedState会替代enhancer变成undefined的。有了这么一层转换之后,我们就可以大胆地第二个参数传enhancer了。

说完createStore的参数,下面我说一下函数createStore执行完之后返回的对象都有什么?在createStore.js最下面一行有这一行代码:

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

他返回了有这么几个方法,其中前三个最为常用,后面两个在项目基本上不怎么用,接下来我们去一一剖析。

定义的一些变量
let currentState = preloadedState //从函数createStore第二个参数preloadedState获得
let currentReducer = reducer  //从函数createStore第一个参数reducer获得
let currentListeners = [] //当前订阅者列表
let nextListeners = currentListeners //新的订阅者列表
let isDispatching = false

其中变量isDispatching,作为锁来用,我们redux是一个统一管理状态容器,它要保证数据的一致性,所以同一个时间里,只能做一次数据修改,如果两个action同时触发reducer对同一数据的修改,那么将会带来巨大的灾难。所以变量isDispatching就是为了防止这一点而存在的。

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.")
    }

    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

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

    return action
  }

函数dispatch在函数体一开始就进行了三次条件判断,分别是以下三个:

判断action是否为简单对象

判断action.type是否存在

判断当前是否有执行其他的reducer操作

当前三个预置条件判断都成立时,才会执行后续操作,否则抛出异常。在执行reducer的操作的时候用到了try-finally,可能大家平时try-catch用的比较多,这个用到的还是比较少。执行前isDispatching设置为true,阻止后续的action进来触发reducer操作,得到的state值赋值给currentState,完成之后再finally里将isDispatching再改为false,允许后续的action进来触发reducer操作。接着一一通知订阅者做数据更新,不传入任何参数。最后返回当前的action。

getState
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相比较dispatch要简单许多,返回currentState即可,而这个currentState在每次dispatch得时候都会得到响应的更新。同样是为了保证数据的一致性,当在reducer操作的时候,是不可以读取当前的state值的。说到这里,我想到之前一次的面试经历:

面试官:执行createStore函数生成的store,可不可以直接修改它的state?

我:可以。(普罗大众的第一反应)

面试官:你知道redux怎么做到不能修改store的state吗?

我:额......(处于懵逼状态)

面试官:很简单啊!重写store的set方法啊!

那会没看过redux的源码,就被他忽悠了!读完redux源码之后,靠!这家伙就是个骗子!自己没读过源码还跟我聊源码,无语了!当然,我自己也有原因,学艺不精,被忽悠了。我们这里看了源码之后,getState函数返回state的时候,并没有对currentState做一层拷贝再给我们,所以是可以直接修改的。只是这么修改的话,就不会通知订阅者做数据更新。得出的结论是:

store通过getState得出的state是可以直接被更改的,但是redux不允许这么做,因为这样不会通知订阅者更新数据。
subscribe
function subscribe(listener) {
    if (typeof listener !== "function") {
      throw new Error("Expected the listener to be a function.")
    }

    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."
      )
    }

    let isSubscribed = true //表示该订阅者在订阅状态中,true-订阅中,false-取消订阅

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      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."
        )
      }

      isSubscribed = false

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

在注册订阅者之前,做了两个条件判断:

判断监听者是否为函数

是否有reducer正在进行数据修改(保证数据的一致性)

接下来执行了函数ensureCanMutateNextListeners,下面我们看一下ensureCanMutateNextListeners函数的具体实现逻辑:

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

逻辑很简单,判断nextListeners和currentListeners是否为同一个引用,还记得dispatch函数中有这么一句代码以及定义变量时一行代码吗?

// Function dispatch
const listeners = (currentListeners = nextListeners)
// 定义变量
let currentListeners = []
let nextListeners = currentListeners

这两处将nextListeners和currentListeners引用了同一个数组,另外定义变量时也有这么一句话代码。而ensureCanMutateNextListeners就是用来判断这种情况的,当nextListeners和currentListeners为同一个引用时,则做一层浅拷贝,这里用的就是Array.prototype.slice方法,该方法会返回一个新的数组,这样就可以达到浅拷贝的效果。

函数ensureCanMutateNextListeners作为处理之后,将新的订阅者加入nextListeners中,并且返回取消订阅的函数unsubscribe。函数unsubscribe执行时,也会执行两个条件判断:

是否已经取消订阅(已取消的不必执行)

是否有reducer正在进行数据修改(保证数据的一致性)

通过条件判断之后,讲该订阅者从nextListeners中删除。看到这里可能有小伙伴们对currentListeners和nextListeners有这么一个疑问?函数dispatch里面将二者合并成一个引用,为啥这里有啥给他俩分开?直接用currentListeners不可以吗?这里这样做其实也是为了数据的一致性,因为有这么一种的情况存在。当redux在通知所有订阅者的时候,此时又有一个新的订阅者加进来了。如果只用currentListeners的话,当新的订阅者插进来的时候,就会打乱原有的顺序,从而引发一些严重的问题。

replaceReducer
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.")
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

这个函数是用来替换reducer的,平时项目里基本很难用到,replaceReducer函数执行前会做一个条件判断:

判断所传reducer是否为函数

通过条件判断之后,将nextReducer赋值给currentReducer,以达到替换reducer效果,并触发state更新操作。

observable
  /**
   * Interoperability point for observable/reactive libraries.
   * @returns {observable} A minimal observable of state changes.
   * For more information, see the observable proposal:
   * https://github.com/tc39/proposal-observable
   */

这里没贴代码,因为这块代码我们不需要掌握。这个observable函数,并没有调用,即便暴露出来我们也办法使用。所以我们就跳过这块,如果有兴趣的话,可以去作者给的github的地址了解一下。

讲完这几个方法之后,还有一个小细节需要说一下,createStore函数体里有这样一行代码。

dispatch({ type: ActionTypes.INIT })

为啥要有这么一行代码?原因很简单,假设我们没有这样代码,此时currentState就是undefined的,也就我说我们没有默认值了,当我们dispatch一个action的时候,就无法在currentState基础上做更新。所以需要拿到所有reducer默认的state,这样后续的dispatch一个action的时候,才可以更新我们的state。

combineReducers.js

这个js对应着redux里的combineReducers方法,主要作用就是合并多个reducer。现在我们先给一个空的函数,然后再一步步地根据还原源码,这样大家可能理解得更为透彻点。

//reducers  Object类型  每个属性对应的值都要是function
export default function combineReducers(reducers) {
    ....
}
第一步:浅拷贝reducers
export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const 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]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
}

这里定义了一个finalReducers和finalReducerKeys,分别用来拷贝reducers和其属性。先用Object.keys方法拿到reducers所有的属性,然后进行for循环,每一项可根据其属性拿到对应的reducer,并浅拷贝到finalReducers中,但是前提条件是每个reducer的类型必须是Function,不然会直接跳过不拷贝。

第二步:检测finalReducers里的每个reducer是否都有默认返回值
function assertReducerShape(reducers) {
  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.`
      )
    }

    const 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, but can be null.`
      )
    }
  })
}

export default function combineReducers(reducers) {
    //省略第一步的代码
    ......
    let shapeAssertionError
    try {
        assertReducerShape(finalReducers)
    } catch (e) {
        shapeAssertionError = e
    }
}

assertReducerShape方法主要检测两点:

不能占用的命名空间

如果遇到未知的action的类型,不需要要用默认返回值

如果传入type为 @@redux/INIT<随机值> 的action,返回undefined,说明没有对未
知的action的类型做响应,需要加默认值。如果对应type为 @@redux/INIT<随机值> 的action返回不为undefined,但是却对应type为 @@redux/PROBE_UNKNOWN_ACTION_<随机值> 返回为undefined,说明占用了 命名空间。整个逻辑相对简单,好好自己梳理一下。

第三步:返回一个函数,用于代理所有的reducer
export default function combineReducers(reducers) {
    //省略第一步和第二步的代码
    ......
    let unexpectedKeyCache
        if (process.env.NODE_ENV !== "production") {
        unexpectedKeyCache = {}
    }
    return function combination(state = {}, action) {
        if (shapeAssertionError) {
            throw shapeAssertionError
        }

        if (process.env.NODE_ENV !== "production") {
            const warningMessage = getUnexpectedStateShapeWarningMessage(
                state,
                finalReducers,
                action,
                unexpectedKeyCache
            )
            if (warningMessage) {
                warning(warningMessage)
            }
        }

        let hasChanged = false
        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") {
            const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
        nextState[key] = nextStateForKey
        hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        return hasChanged ? nextState : state
    }    
}

首先对传入的state用getUnexpectedStateShapeWarningMessage做了一个异常检测,找出state里面没有对应reducer的key,并提示开发者做调整。接着我们跳到getUnexpectedStateShapeWarningMessage里,看其实现。

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"

  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."
    )
  }

  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("", "")}"`
    )
  }

  const unexpectedKeys = Object.keys(inputState).filter(
    key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
  )

  unexpectedKeys.forEach(key => {
    unexpectedKeyCache[key] = true
  })

  if (action && action.type === ActionTypes.REPLACE) return

  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接收四个参数 inputState(state)、reducers(finalReducers)、action(action)、unexpectedKeyCache(unexpectedKeyCache),这里要说一下unexpectedKeyCache是上一次检测inputState得到的其里面没有对应的reducer集合里的异常key的集合。整个逻辑如下:

前置条件判断,保证reducers集合不为{}以及inputState为简单对象

找出inputState里有的key但是 reducers集合里没有key

如果是替换reducer的action,跳过第四步,不打印异常信息

将所有异常的key打印出来

getUnexpectedStateShapeWarningMessage分析完之后,我们接着看后面的代码。

    let hasChanged = false
    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") {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    return hasChanged ? nextState : state

首先定义了一个hasChanged变量用来表示state是否发生变化,遍历reducers集合,将每个reducer对应的原state传入其中,得出其对应的新的state。紧接着后面对新的state做了一层未定义的校验,函数getUndefinedStateErrorMessage的代码如下:

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.`
  )
}

逻辑很简单,仅仅做了一下错误信息的拼接。未定义校验完了之后,会跟原state作对比,得出其是否发生变化。最后发生变化返回nextState,否则返回state。

compose.js

这个函数主要作用就是将多个函数连接起来,将一个函数的返回值作为另一个函数的传参进行计算,得出最终的返回值。以烹饪为例,每到料理都是从最初的食材经过一道又一道的工序处理才得到的。compose的用处就可以将这些烹饪工序连接到一起,你只需要提供食材,它会自动帮你经过一道又一道的工序处理,烹饪出这道料理。

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

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

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

上面是es6的代码,可能小伙伴们并不是很好理解,为了方便大家理解,我将其转换成es5代码去做讲解。

function compose() {
  var _len = arguments.length;
  var funcs = [];
  for (var i = 0; i < _len; i++) {
    funcs[i] = arguments[i];
  }

  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  }

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

  return funcs.reduce(function (a, b) {
    return function () {
      return a(b.apply(undefined, arguments));
    };
  });
}

梳理一下整个流程,大致分为这么几步:

新建一个新数组funcs,将arguments里面的每一项一一拷贝到funcs中去

当funcs的长度为0时,返回一个传入什么就返回什么的函数

当funcs的长度为1时,返回funcs第0项对应的函数

当funcs的长度大于1时,调用Array.prototype.reduce方法进行整合

这里我们正好复习一下数组的reduce方法,函数reduce接受下面四个参数

total 初始值或者计算得出的返回值

current 当前元素

index 当前元素的下标

array 当前元素所在的数组

示例:

const array = [1,2,3,4,5,6,7,8,9,10];
const totalValue=array.reduce((total,current)=>{
  return total+current
}); //55

这里的compose有个特点,他不是从左到右执行的,而是从右到左执行的,下面我们看个例子:

const value=compose(function(value){
  return value+1;
},function(value){
  return value*2;
},function(value){
  return value-3;
})(2);
console.log(value);//(2-3)*2+1=-1

如果想要其从左向右执行也很简单,做一下顺序的颠倒即可。

===> 转换前 return a(b.apply(undefined, arguments));
===> 转换后 return b(a.apply(undefined, arguments));
applyMiddleware.js
export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }

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

    return {
      ...store,
      dispatch
    }
  }
}

前面我们讲enhancer的时候,提到过这个applyMiddleware,现在我们将二者的格式对比看一下。

// enhancer
 function enhancer(createStore) {
    return (reducer,preloadedState) => {
         //逻辑代码
        .......
    }
 }
//applyMiddleware
function //applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        //逻辑代码
        ....... 
    }
 }

通过二者的对比,我们发现函数applyMiddleware的返回就是一个enhancer,下面我们再看其具体实现逻辑:

通过createStore方法创建出一个store

定一个dispatch,如果在中间件构造过程中调用,抛出错误提示

定义middlewareAPI,有两个方法,一个是getState,另一个是dispatch,将其作为中间件调用的store的桥接

middlewares调用Array.prototype.map进行改造,存放在chain

用compose整合chain数组,并赋值给dispatch

将新的dispatch替换原先的store.dispatch

看完整个过程可能小伙伴们还是一头雾水,玄学的很!不过没关系,我们以redux-thunk为例,模拟一下整个过程中,先把redux-thunk的源码贴出来:

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;

哈哈哈!看完redux-thunk的源码之后是不是很奔溃,几千star的项目居然就几行代码,顿时三观就毁了有木有?其实源码没有大家想象的那么复杂,不要一听源码就慌。稳住!我们能赢!根据redux-thunk的源码,我们拿到的thunk应该是这样子的:

 const thunk = ({ dispatch, getState })=>{
    return next => action => {
        if (typeof action === "function") {
            return action(dispatch, getState);
        }
        return next(action);
    };
 }  

我们经过applyMiddleware处理一下,到第四步的时候,chain数组应该是这样子的:

const newDispatch;
const middlewareAPI={
  getState:store.getState,
  dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const  fun1 = (next)=>{
  return action => {
    if (typeof action === "function") {
        return action(dispatch, getState);
    }
    return next(action);
  }
}
const chain = [fun1]

compose整合完chain数组之后得到的新的dispatch的应该是这样子:

const newDispatch;
const middlewareAPI={
  getState:store.getState,
  dispatch: (...args) => newDispatch(...args)
}
const { dispatch, getState } = middlewareAPI;
const next = store.dispatch;
newDispatch = action =>{
  if (typeof action === "function") {
    return action(dispatch, getState);
  }
  return next(action);
}

接下来我们可以结合redux-thunk的例子来模拟整个过程:

function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize("The Sandwich Shop", forPerson, error))
    );
  };
}
// store.dispatch就等价于newDispatch
store.dispatch(makeASandwichWithSecretSauce("Me"))

====> 转换
const forPerson = "Me";
const action = (dispatch)=>{
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize("The Sandwich Shop", forPerson, error))
    );
}
newDispatch()

===> typeof action === "function" 成立时

 ((dispatch)=>{
    return fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize("The Sandwich Shop", forPerson, error))
    );
  })( (...args) => newDispatch(...args), getState)

====> 计算运行结果
const forPerson = "Me";
const dispatch = (...args) => newDispatch(...args) ;
fetchSecretSauce().then(
      sauce => dispatch(makeASandwich(forPerson, sauce)),
      error => dispatch(apologize("The Sandwich Shop", forPerson, error))
);
// 其中:
function fetchSecretSauce() {
  return fetch("https://www.google.com/search?q=secret+sauce");
}
function makeASandwich(forPerson, secretSauce) {
  return {
    type: "MAKE_SANDWICH",
    forPerson,
    secretSauce
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: "APOLOGIZE",
    fromPerson,
    toPerson,
    error
  };
}
====> 我们这里只计算Promise.resolve的结果,并且假设fetchSecretSauce返回值为"666",即sauce="666"

const forPerson = "Me";
const dispatch = (...args) => newDispatch(...args) ;
dispatch({
    type: "MAKE_SANDWICH",
    "Me",
    "666"
})
====> 为了方便对比,我们再次转换一下

const action = {
    type: "MAKE_SANDWICH",
    "Me",
    "666"
};

const next = store.dispatch

const newDispatch = action =>{
  if (typeof action === "function") {
    return action(dispatch, getState);
  }
  return next(action);
}

newDispatch(action)

====> 最终结果
store.dispatch({
    type: "MAKE_SANDWICH",
    "Me",
    "666"
});

以上就是redux-thunk整个流程,第一次看肯能依旧会很懵,后面可以走一遍,推导一下加深自己的理解。

bindActionCreators.js
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 keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

bindActionCreators针对于三种情况有三种返回值,下面我们根据每种情况的返回值去分析。(为了方便理解,我们选择在无集成中间件的情况)

typeof actionCreators === "function"
function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}
const actionFun=bindActionCreator(actionCreators, dispatch)

===> 整合一下

const fun1 = actionCreators;
const dispatch= stror.dispatch;
const actionFun=function () {
    return dispatch(fun1.apply(this, arguments))
 }

根据上面的推导,当变量actionCreators的类型为Function时,actionCreators必须返回一个action。

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"?`
    )

提示开发者actionCreators类型错误,应该是一个非空对象或者是函数。

默认
 const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators

通过和第一种情况对比发现,当actionCreators的每一项都执行一次第一种情况的操作。换句话说,默认情况是第一种情况的集合。

以上是对bindActionCreators的剖析,可能小伙伴们对这个还是不够理解,不过没有关系,只要知道bindActionCreators干了啥就行。bindActionCreators是需要结合react-redux一起使用的,由于本篇文章没有讲解react-redux,所以这里我们不对bindActionCreators做更深入的讲解。下篇文章讲react-redux,会再次提到bindActionCreators。

结语

到这里整个redux的源码我们已经剖析完了,整个redux代码量不是很大,但是里面的东西还是很多的,逻辑相对来说有点绕。不过没关系,没有什么是看了好几次都看不懂的,如果有那就再多看几次嘛!另外再多一嘴,如果想快读提高自己的小伙伴们,我个人是强烈推荐看源码的。正所谓“近朱者赤,近墨者黑”,多看看大神的代码,对自己的代码书写、代码逻辑、知识点查缺补漏等等方面都是很大帮助的。就拿我自己来说,我每次阅读完一篇源码之后,都受益匪浅。可能第一次看源码,有着诸多的不适应,毕竟万事开头难,如果强迫自己完成第一次的源码阅读,那往后的源码阅读将会越来越轻松,对自己的提升也就越来越快。各位骚年们,撸起袖子加油干吧!

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

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

相关文章

  • Redis 哈希结构内存模型剖析

    摘要:本文共字,阅读大约需要分钟概述在前文字符串类型内部编码剖析之中已经剖析过最基本的类型的内部是怎么编码和存储的,本文再来阐述中使用最为频繁的数据类型哈希或称散列,在内部是怎么存的。 showImg(https://segmentfault.com/img/remote/1460000016158153); 本文共 1231字,阅读大约需要 5分钟 ! 概述 在前文《Redis字符串类型...

    Salamander 评论0 收藏0

发表评论

0条评论

CloudwiseAPM

|高级讲师

TA的文章

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