手摸手阅读redux、redux-thunk源码

1,101 阅读13分钟

看了一些库的源码,再看redux的源码过后,觉得redux源码算是比较简单的一个了,在之前博主也想过去看一些redux源码的解析文章,但是都是断断续续的,也是似懂非懂,而我呢,喜欢的是一行一行的去看,不然的话,虽然知道了大概的原理,但是一些实现细节还是不知道。

于是我就索性自己把仓库拉下来了,自己去一行一行的看,写注释等。本篇文章将会一行一行的阅读redux源码,帮助像我这种小白去理解redux的原理。 这里附上仓库的地址: --redux仓库--

image.png

这里说一个前提: 看一个东西的源码之前,首先得明白怎么去使用它,如果你还不会使用redux,那么请移步:

  1. redux中文文档
  2. redux入门教程

我们需要会使用过后,带着一些疑问去阅读源码,才会有所收获:

  1. subscrib怎么执行更新的?
  2. combineReducer怎么根据name区分的?
  3. 中间件怎么生效的?
  4. 。。。

并且了解了redux之后,才可以去了解useDispatchuseSelector等hook。

起步

好的,让我们开始吧。 当把源码拉下来过后,我们打开目录下的src文件夹,会发现如下的几个文件

  1. applyMiddleWare.ts
  2. bindActionCreators.ts
  3. combineReducers.ts
  4. compose.ts
  5. createStroe.ts
  6. index.ts

我们需要着重于看标橙色的文件的实现。

首先我们来看看我们平常使用redux的时候会做什么:

  1. 初始化state,创建reducer函数
  2. 从redux库引入createStore函数,再调用
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer)
export default store

所以说我们redux的入口就是createStore函数了,所以我们直接打开createStore.ts文件。

createStore

作为redux的入口函数,也是redux的核心函数。 可以看到这个文件最后就一个作用:导出了createStore函数,总的文件结构大致如下(省略ts语法):

export default function createStore(reducer, preloadedState, enhancer) {

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

  //...除功能函数外省略...
  function ensureCanMutateNextListeners() {}

  //使用getState获取store的最新值
  function getState() {}

  //订阅store,传入监听函数,当store值发生变化就会依次执行监听函数
  function subscribe(listener) {
    //取消订阅store的函数
    return function unsubscribe() {}
  } 
   
  // 通过dispatch完成store值得更新
  function dispatch(action) {}

  //使用现有的reducer替换原来的reducer
  function replaceReducer(nextReducer) {}
 
  //暂时还不知道,后面再看
  function observable() {}
   
  dispatch({ type: ActionTypes.INIT })

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

可以看到,在执行createStore的时候,除了一些函数的声明之外,只执行了一段代码:

dispatch({ type: ActionTypes.INIT })

getState

在看这段代码之前,由于我知道getState是去获取当前store的值,所以我先去看了一下getState,getState的详细代码如下:

  function getState(): S {
    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
  }

其实就是返回了currentState,但是我们看createStore函数内的前几句代码中有这么一句:

let currentState = preloadedState

而这个preloadedState则是函数的第二个参数,由于我们在使用的时候只传入了reducer,并未传入第二个参数,也就是说,当前的crrentState的值为undefined,那么为什么我们使用getState的时候,能够得到store的值呢??尤其是第一次使用的时候,明明是undefined,怎么就有值了呢?

带着这个问题,我想到了刚刚的那一段代码:

dispatch({ type: ActionTypes.INIT })

dispatch

这段代码是createStore除了声明函数和返回值以外,执行的唯一功能性代码,那么crrentState的值,肯定就是这一段代码中发生改变的。由于涉及到dispatch函数,我们现在就来看看这个函数中到底做了什么事情。

 function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        `Actions must be plain objects. Instead, the actual type was: "${kindOf(
          action
        )}". You may need to add middleware to your store setup to handle dispatching other values, such as "redux-thunk" to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
    }

    if (typeof action.type === "undefined") {
      throw new Error(
        "Actions may not have an undefined "type" property. You may have misspelled an action type string 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是否是是通过字面量形式或者new Object()形式定义的对象。
    if (!isPlainObject(action)) {
      throw new Error(
        `Actions must be plain objects. Instead, the actual type was: "${kindOf(
          action
        )}". You may need to add middleware to your store setup to handle dispatching other values, such as "redux-thunk" to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
      )
    }

    //查看是否传入了type属性
    if (typeof action.type === "undefined") {
      throw new Error(
        "Actions may not have an undefined "type" property. You may have misspelled an action type string constant."
      )
    }

    if (isDispatching) {
      throw new Error("Reducers may not dispatch actions.")
    }


首先我们会对传入的action做判断,isPlainObject的作用就是判断==action是否是是通过字面量形式或者new Object()形式定义的对象。==,然后再判断action是否有type属性,因为我们想要dispatch一个action的话,是需要一个type属性去执行条件判断从而执行特定的代码的。而最后一个判断,isDispatching这个变量在开始也声明了为false,所以这里不用管,根据后面的代码,大概意思就是是否正在dispatch,类似于一个状态锁吧。

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

这段代码会将isDispatching置成true,意味着正在dispatch的过程中,然后下面一段代码给currentState 赋值了,所以我就会想到,currentState一定就是在这里改变的。并且currentReducer就是我们函数的第一个参数,也就是我们的reducer。让我们回顾一下我们reducer的写法:

const defaultState = {
	count:1,
 	name:"丁时一"
}
function reducer(state=defaultState,action){
	switch(action.type){
		case "ADD":
 			return {...state,count:coun+1};
		default:
			return state
	}
}

既然如此,这里的crrentReducer就相当于我们当前写的reducer函数,由于currentReducer当前值是undefined,所以reducer的state的值为我们的defaultState,并且我们传入的特定type没有在我们的switch的范围内,所以我们会执行默认的default从而返回原本的state也就是defualtState,并且将这个值赋值给currentState

哦~~~~ 我好像明白了,createStore执行的时候除了声明功能函数和导出数据之外,只做了一件事,那就是通过执行==dispatch({ type: ActionTypes.INIT })==,来对currentState进行初始化,值为我们定义的state,这样我们在执行完createStore过后,使用getState就能够获取到我们的state数据了。

这个疑问到这里就结束了,是不是很开心。 image.png

但是我们的dispatch函数还没结束呢,后面还有这么一段代码:

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

这段代码的currentListeners其实在函数最开始也声明了,但是也是一个空数组,所以这里也不会执行,但是这段代码肯定有他的作用啊,从代码的意思来看,就是把数组里的每一项拿出来执行,也就是说,这个数组里面存的是函数。 看到这里,兄弟们是否想到了什么? 没想到没关系,但是我们在用redux的时候,肯定会使用store.subsubscribe吧?

const unsubscribe  = store.subscribe(()=>{
	const state = store.getState();
        this.setState({/***/})
})

subscribe

在我们使用中,我们了解到,可以订阅store,当我们使用dispatch去改变state数据的时候,我们传入的函数就会被执行,然后我们在这个函数里面使用getState去获取最新的state的值,然后再去一些UI的重新渲染。

那么我们来看看subscribe的功能呢?先来看看代码:

  function subscribe(listener) {
    if (typeof listener !== "function") {
      throw new Error(
        `Expected the listener to be a function. Instead, received: "${kindOf(
          listener
        )}"`
      )
    }

    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/store#subscribelistener for more details."
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()

    //=====重点=====
    nextListeners.push(listener)

    return function unsubscribe() {
 
  }

首先就是判断我们传入的listener是不是一个函数,因为我们dispatch要执行listener(),如果不是函数就会报错了。

其次会判断是否正在isDispatching中,这个先不用管。

这里最主要的一段代码就是: 我们把传入的listner给收集起来了,那么我们在之后执行dispatch的时候,那么就会把这里面的listener全部取出来调用。 这也就是subscribe核心,其实就是发布订阅~~~~

nextListeners.push(listener)

unsubscribe

并且我们在使用的时候,知道subscribe会返回一个函数,我们在组件销毁的时候执行这个函数,就可以取消订阅了,那么我们来看看subscribe的返回值:

    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/store#subscribelistener for more details."
        )
      }

      isSubscribed = false

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

其实这个函数的作用,就是通过indexOf找到我们监听函数在数组中的位置,然后再通过splice把数组中的这一项给移除掉了, 这样我们就可以达到取消订阅的效果拉~~

看到这里,大家会不会有一个疑问: 这个返回值这么多代码,就只讲这一句,你是不是不会啊?

  1. ensureCanMutateNextListeners有啥作用啊?
  2. 为啥会有currentListeners和nextListenrs啊?一个不行吗?
  3. isSubscribed是啥,也是状态锁吗?
  4. 。。。。

说真的,我之前真不会,不过之后自己也弄清楚了

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      //浅拷贝
      nextListeners = currentListeners.slice()
    }
  }

这个函数的作用就是当nextListeners和currentListeners相等的时候,做一次浅拷贝,那么我们发现,这两个相等,也会是会有赋值的语句就下面这些:

//函数一开始的
  let currentListeners = []
  let nextListeners = currentListeners

//dispatch里面的
  const listeners = (currentListeners = nextListeners)

但是好像也没发现什么,为什么要一个浅拷贝呢?

为了更清楚的讲述,我打算用一个例子: 我们来看下面的代码:

let arr = []
function f1() {
    console.log(1111);
    arr.push(() => {
        console.log(2222);
    })
}
function f2() {
    console.log(33333);
}
arr = [f1, f2];
for (let i = 0; i < arr.length; i++) {
    arr[i]()
}

我们在arr中放入了f1,f2 并且f1执行中会往arr中再放入一个函数, 让我们循环执行arr的元素,看看结果呢? image.png

结果很显然,我们在f1中 添加到arr中的函数也执行了。 但是如果换做我们redux的subscribe,我们只需要执行我们当前的一些方法,至于在方法中再次订阅的方法,我们本次并不需要执行,会等到下一个dispatch去执行,这样可以避免一些嵌套订阅从而导致无法按照正常的逻辑来执行的bug。

现在我在redux中去做了这个实验 来看看我们的redux的结果 :

function App(props) {
  const { store } = props;
  console.log(store);
  store.subscribe(() => {
    console.log("执行第 1 次订阅的函数");
    store.subscribe(() => {
      console.log("执行第 2 次订阅的函数");
    })
  })
  return (
    <div className="App">
      <button onClick={() => { store.dispatch({ type: INCREAMENT, payload: 1 }) }} >dispatch</button>
    </div>
  );
}

当我们第一次点击触发dispatch的时候,只会执行第一次订阅的函数,第二次点击才会触发全部的2个函数 image.png

那么这个功能是怎么实现的呢? 那就要归功于 ensureCanMutateNextListeners了,可以看到这个函数的作用就是把nextListeners作为currentListeners的一个浅拷贝,在subscribe的时候,往nextListenr中添加listner之前,就做一次浅拷贝

    //浅拷贝
    ensureCanMutateNextListeners()
    //把监听函数都收集起来,盲猜后面数据更新的时候会全部执行一遍
    nextListeners.push(listener)

如果这时候遇到了嵌套的情况,正在执行下面这段代码的时候,又发生了订阅往nextListenrs中添加了方法,由于在添加之前会做一次浅拷贝,所以这里也不会影响到currentListeners 和listeners了,所以因为嵌套添加的方法不会在currentListeners中,而是在nextListeners中,所以不会影响本次的执行结果。

    const listeners = (currentListeners = nextListeners)
    //第一次dispatch的时候这里不会执行
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

基于这个方法,我们来改进一下我们的例子:

let temparr = arr = []
function f1() {
    console.log(1);
    arr = temparr.slice()
    arr.push(() => {
        console.log(3);
    })
}
function f2() {
    console.log(2);
}
arr = [f1, f2];


function runArr() {
    temparr = arr;
    for (let i = 0; i < temparr.length; i++) {
        arr[i]()
    }
}

runArr()
console.log("第一次执行完毕");

runArr()
console.log("第二次执行完毕");

执行结果如下: image.png

是不是突然就解决了这个疑问,是不是就知道了为啥==redux中嵌套订阅也不会有逻辑问题啦,就是浅拷贝的功劳!!!==

对于取消订阅越是一样的操作!

至于observablereplaceReducer我还没用过。。。(打脸了),之后再补上。 到这里 createStore,也就是redux的核心也就结束了。接下来的一些就是在对这个功能进行扩展啦~~

那么说到扩展,我们肯定要讲讲combineReducers

combineReducers

combineReducers 允许我们合并多个reducer,并且可以以key值的方式命名。

可能有些同学没有使用过combineReducers,那么我说一下我的理解: 我们在开发的过程中,很少会把所有的状态都放在一个store文件里吧?我们肯定会为特定的模块去分离我们的state、reducer、action等,其实就是类似于==vuex的moudles==,把每个modules分开,最后组合到主modules里面,不过redux是通过combineReducers来组合reducer达到目的的,是不是就清楚了许多?嘿嘿~

其实博主之前开发也是这么做的,每个模块分离对应的store。

然后再把对应的store整合起来。

//合并reducer
import { combineReducers } from "redux-immutable";
//首页
import { reducer as homeReducer } from "@/pages/home/store";
//头部
import { reducer as headerReducer } from "@/components/header/store/index";
//详情页
import { reducer as detailReducer } from "@/pages/detail/store";

//.....
//.....
const cReducer = combineReducers({
  home: homeReducer,
  header: headerReducer,
  detail: detailReducer,
  //......
});

export default cReducer;

然后再把这个cReducer传入我们createStore就好了。那么我在了解createStore后再看到这个,我会有几个疑问:

  1. state都在每个reducer里,怎么把对应的state全部整合到一个里面呢?
  2. 整合过后的state怎么实现对应更新的呢?

带着这些问题,我们来看看combineReducers的实现过程

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 (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)
  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  return function combination(state = {}, action) {
	//....
  }
}

combineReducers接受一个参数reducers,这个参数就是我们很多个reducer组成的对象,

第一个for循环的作用就是把reducers拷贝到finalReducers当中然后执行了:

 assertReducerShape(finalReducers)

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

    if (
      typeof reducer(undefined, {
        type: ActionTypes.PROBE_UNKNOWN_ACTION(),
      }) === "undefined"
    ) {
      throw new Error("**")
    }
  })
}

这个assertReducerShape函数的作用就是检查我们传入的reducers当中,有没有不合格的,比如没有默认返回值的。如果有不合格的,就抛出错误。

那么如果所有的reduce都是正常的,那么我们就返回一个函数,从使用combineReducers的例子来看,combineReducers的返回值就是我们合并后的reudcer了,所以返回值就相当于一个新的reducer,肯定也是要接收state和type参数啦,让我们来看看:

  return function combination(state = {}, action) {
    //省略....


    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 actionType = action && action.type
        throw new Error("**")
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }

先不着急解说,就说大家看到这里,有么有解决之前的两个问题? state怎么合并到一起的? 哈哈哈哈 没有吧,我当时也不知道,后来看着也有点绕,没事,马上就知晓了~

首先有一个hasChanged变量,这个变量标志着我们整合后的state在dispatch后有没有发生变化,这里可能大家就会有一个疑问了:==dispatch就是改变state的,怎么会没有发生变化呢?==,诶,如果我们dispatch的type不在reducer的条件范围里面呢,是不是就还是返回默认的state,也就是上次的state哇, 这个hasChange就是做这个标记的。

之后定义了一个变量nextState,这个变量就是拿来存储我们合并和的state的,我们继续往下看。 在这之后会有一个for循环,拿到我们给reducer对应的key值和reducer本身,然后把previousStateForKey,和action传入我们的reducer,得到执行的返回值,就是nextStateForKey了,这里比较重要,因为我们第一次执行reducer,如果传入的state是undefined或者null,那么state就是我们默认的state,正好我们的previousStateForKey当前的undefiend(因为在我们返回的函数中,state={}),所以执行reducer后的返回值就是每个reducer的state了~,最后,我们通过key-value的形式,把name和state都存到nextState中,然后对比前后的state的值得到hasChanged的值

   nextState[key] = nextStateForKey
   hasChanged = hasChanged || nextStateForKey !== previousStateForKey

第一次的时候,前后的值从{}到合并后的state肯定发生了变化,所以hasChanged肯定为true,所以返回的就是nextState,也就是我们合并后的state,那么再加上我们知道在createStore中,getState得到的是reducer的返回值,所以getState就能够获取到我们合并后的state了。

 return hasChanged ? nextState : state

到了这里==state怎么合并到一起的==?是不是就解决啦~~ image.png

那么还剩下一个问题:==整合过后的state怎么实现对应更新的呢==?

因为合并了那么多state,在我们执行一个dispatch的时候,怎么去更新这个state的呢?毕竟每个state都是通key-value的形式存储的,就像这样:

state = {
  home:{
    page:1,
    articles:[]
  },
  user:{ 
    name:"丁时一"
    age:22
  }
	
}

假如我只修改page,到底是怎么去更新的呢? 带着疑问,就去看看这个更新的过程。

如果我们执行了dispatch({type:CHANGE_NAME}),那么我们就会去执行我们返回的那个函数,也就是

return function combination(state = {
  home:{
    page:1,
    articles:[]
  },
  user:{ 
    name:"丁时一"
    age:22
  }, action) {
 //...
}

然后我们就会在下面的for循环中,拿到全部的reducer,把当前的state和type传进去,如果没有对这个type进行判断的reducer肯定还是返回原来的state,只有home模块里面的reducer才会对这个type作出反应,返回处理之后的home_state,然后再把这些state(不管变没有变)都存到nextState当中。

    for (let i = 0; i < finalReducerKeys.length; i++) {
      //省略。。。
      //每个reducer都把type和这个reducer之前的state传进去了。
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
  }

既然home_state前后不同了那么hasChanged是不是也就是为true了,

所以说最后就返回了最新的nextState

    return hasChanged ? nextState : state

如果我们传入的type让所有的reducer都返回了默认值,那么hasChanged就是false,最后会返回之前的state。

总的来说,不是准确的去执行某一个reducer,是把全部的reducer都执行后,看看全部的状态有没有发生变化。

我超,是不是很神奇~~~!!! image.png

compose

本来是想先看applyMiddleWare的,但是发现applyMiddleWare中用到了compose去做一些链式的处理,所以得先学学compose

compose的代码很少,但是第一次看还是每太懂,前面两个条件判断还好

  1. 如果没有参数,那么我们就自定义的返回一个函数
  2. 如果只有一个函数参数,那么直接返回执行的结果
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)))
}

只是最后的一段代码,看到reduce+返回一个function的组合,第一时间我也是懵的。 首先我们得知道reduce的使用方法,reduce的第一个参数接收一个函数,并且函数是一个"积累器",就是函数本次的执行结果,会作为函数在下一次执行的第一个参数,并且==如果我们没有传入第二个参数(也就是初始参数),那么第一次执行的时候,a的值就是args的第一个值==

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

//为了方便阅读,我把它转换成function的形式
return funcs.reduce(function(a,b){
	return function(...args){
		return a(b(...args))
	}
})

光这么说没用,还是要举个例子:

let x = 10;
function fn1(x){
	return x+1
}
function fn2(x){
	return x+2
}
function fn3(x){
	return x+3
}

const newFn = compose(fn1,fn2,fn3)
//我们可以从这个中去入手,

首先因为没有传初始值,所以a的值就是fn1,b的值就是fn2。然后第一次的返回值就是如下的函数

function (...args){
      return fn1(fn2(...args))
}

并且这个函数会作为下一次的a,第二次的时候,a就是上方的函数,而b就是fn3 所以我们会发现 最后的newFn的值为:

function (...args){
	return fn1(fn2(fn3(...args)))
}

所以我们最后执行newFn的时候得到的值就是: 到了这里大概就知道了compose的作用了,就是把==f1(f2(f3(x)))的方式转换成 (f1,f2,f3)(x)的这种方式了==

	fn1(fn2(fn3(...args)))
	//加入我传入x为10 最后的值 就是16

applyMiddleWare

了解到compose的作用之后,我们就可以来瞅瞅作为redux扩展的利器applyMiddleware

applyMiddleware的代码量并不多,只有20来行,但是这20来行确确实实是有难度的。f

export default function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error()
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }
    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}

这里就有个问题了: 这段代码我能看懂,但是作用我不知道啊。。。我知道我为啥不懂了,因为我还不熟悉中间件的使用

我觉得最好的例子就是找到一个中间件,带着这个中间件的代码,再过一遍applyMiddleWare,这样应该会好很多。 既然说到这中间件了,那就说一下我的项目中用到的一个中间件redux-thunk吧。applyMiddleWare先抛开。

redux-thunk

什么14行代码的中间件有17k的star?

首先,我们来看看redux-thunk的使用方法:

import { createStore, applyMiddleware, compose } from "redux";
import reducer from "./reducer";
import thunk from "redux-thunk";
const store = createStore(reducer,applyMiddleware(thunk));

export default store;


//如何使用??
function getData(){
    return dispatch=>{
	Axios.get(...).then(res=>{
	  dispatch(changeData(res))	
	})
    }
}

function changeData(res){
	return {
	    type:"CHANGE_DATA",
	    res
	}
}

//在组件中使用

const dispatch = useDispatch();
dispatch(getData())

可以看到,在使用redux-thunk之后呢,允许我们可以dispatch一个function了,然后在function里面去进行异步,最后再真实的通过dispatch去触发reducer更新state。

那么现在就有一些疑问了:

  1. 为什么可以dispatch一个函数了呢?
  2. getData的返回函数的参数dispatch为什么才是真正的dispatch?
  3. 中间件触发的时机是什么时候

了解了redux-thunk的使用方法,让我们带着疑问去阅读一下实现原理吧! 首先我们把redux-thunk的源代码:

function createThunkMiddleware(extraArgument) {
    var middleware = function middleware(_ref) {
        var dispatch = _ref.dispatch,
            getState = _ref.getState;
        return function(next) {
            return function(action) {
                if (typeof action === "function") {
                    return action(dispatch, getState, extraArgument);
                }
                return next(action);
            };
        };
    };
    return middleware;
}
var thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
var _default = thunk;
exports.default = _default;

首先我们看到最导出了default,而 default就是我们的thunk,而我们使用的时候,就是createStore(reducer,applyMiddleWare(thunk))

所以我们需要会过头去看看createStore,我们会发现createStore当中会对第二个参数进行判断: 当我们第二个参数传中间件,那么第三个参数ehancer就是undefined,满足条件,所以现在的enhancer就是我们传入的applyMiddleWare(thunk)。紧接着来到第二个判断,显然我们最后会执行reture的那一句,最后的return 相当于 ==return applyMiddleWare(thunk)(createStore)(reducer,preloadedState)==


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

  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error()
    }
    return enhancer(createStore)(reducer, preloadedState)
  }

那到了这里,我们就只需要分析==return applyMiddleWare(thunk)(createStore)(reducer,preloadedState)==这句代码就好了,其实细心的同学可能会注意到,既然到了这里就结束了,那么下面的dispatch,subscrib、getState什么都还没定义啊,怎么初始化的呢?

没关系,我们接着往下看。你再次把applyMiddleWare的代码贴出来。可以看到,当前只接收了thunk一个中间件,返回了一个接受createStore的函数,正好我们在上面黄色的句子中把createStore传入了,那我们接着来看,接收createStore的函数又返回了一个函数,并且参数是...args,那我们最后传入的reducer,preloadedState就是...args,拿到这个args,我们再次执行createStore,但这一次执行和没有中间件的执行是一模一样的,定义方法,调用dispatch初始化state,返回这些函数等,最后我们把这个store返回出去了。

相比于没有中间件的调用方法,这种方法只不过是相当于中间做了一些事情, 最后还是把原来的数据给返回了。

export default function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error()
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }
    const chain = middlewares.map((middleware) => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}

我们拿到store过后,会有这么一句话(下方),刚开始我真的不太懂,既然dispatch是拿来调用的,为啥是一个==一调用就抛出错误的函数呢?== 我们这个先记下, 暂且搁置一下。

    let dispatch = () => {
      throw new Error()
    }

接着看后面的代码, 这里就是想到与给增强中间件,给中间件加上getState,dispatch的功能。但是这个dispatch还是那个抛出错误的那个dispatch。 也暂且搁置一下。

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args),
    }

接下来就是拿到我们传入的中间件进行遍历,添加middleWareAPI,

    const chain = middlewares.map((middleware) => middleware(middlewareAPI))

这里因为我们只传入了thunk,所以这里的middleware就是thunk,我们可以看到thunk接收一个_ref参数,并返回了一个函数,所以我们这里chain的值就是一个数组,数组里面有一个函数,也就是thunk执行后返回的那个函数,

紧接着有一句代码,用到了compose,也就是之前为什么要先学compose的原因:这里有几个疑问,

  1. 为什么dispatch被重写了,为什么middleWare里的dispatch是抛出错误的函数,而返回的dispatch并不是
  2. dispatch被重写成了什么?
dispatch = compose(...chain)(store.dispatch)

带着疑问,我们来分析一下这句话,其实这个compose在这里就相当于 chain[0],(store.dispatch),这个chain[0]呢就是thunk返回的那个函数,我把它贴下来:

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

这里的next就是真正的store.dispatch, 那么我们最后调用的dispatch就是上面的函数了,到这里,可能大家(我)已经晕了,来个例子把,就拿刚开始的那个例子:

//如何使用??
function getData(){
    return dispatch=>{
	Axios.get(...).then(res=>{
	  dispatch(changeData(res))	
	})
    }
}

function changeData(res){
	return {
	    type:"CHANGE_DATA",
	    res
	}
}

//在组件中使用

const dispatch = useDispatch();
dispatch(getData())

我们dispatch了一个函数,就是相当于把这个函数作为action参数传入了,那么我们会判断action的类型,如果是函数的话,就执行:

return action(dispatch, getState, extraArgument);

所以我们相当于是执行了

dispatch=>{
	Axios.get(...).then(res=>{
	  dispatch(changeData(res))	
	})

相当于最后执行了dispatch({type:"CHANGE_DATA",res}),而当前的dispatch是什么呢? 这个dispatch是middlewareAPI中的,函数值为:

function dispatch() {
     return _dispatch.apply(void 0, arguments);
}

这里又有疑问了,tmd这个_dispatch不是一个抛出错误的函数嘛。 确实是,但是_dispatch在后面被重写啦(忘记了吗?)

  _dispatch = compose.apply(void 0, chain)(store.dispatch);

我们由这句话得出的就是我们的next就是我们的dispatch,所以我们还会再执行一次next,不过这次的acion可不是函数了,而是对象了,所以就会执行 ==return next(action);== 而我们前面也说过,我们的next才是真正的store.dispatch,所以我们就可以拿到action去更新state拉~

return next(action);

。。。。。确实是有点绕,thunk的代码不多,但确实nb啊。

一个中间件就够绕了,多个中间件。。。。算了。。。我再学习学习再讲。。。。呜呜呜 image.png

自定义中间件

总结

  1. 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客
  2. 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客
  3. 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客