看了一些库的源码,再看redux的源码过后,觉得redux源码算是比较简单的一个了,在之前博主也想过去看一些redux源码的解析文章,但是都是断断续续的,也是似懂非懂,而我呢,喜欢的是一行一行的去看,不然的话,虽然知道了大概的原理,但是一些实现细节还是不知道。
于是我就索性自己把仓库拉下来了,自己去一行一行的看,写注释等。本篇文章将会一行一行的阅读redux源码,帮助像我这种小白去理解redux的原理。 这里附上仓库的地址: --redux仓库--
这里说一个前提
: 看一个东西的源码之前,首先得明白怎么去使用它,如果你还不会使用redux,那么请移步:
我们需要会使用过后,带着一些疑问去阅读源码,才会有所收获:
- subscrib怎么执行更新的?
- combineReducer怎么根据name区分的?
- 中间件怎么生效的?
- 。。。
并且了解了redux之后,才可以去了解useDispatch
,useSelector
等hook。
起步
好的,让我们开始吧。 当把源码拉下来过后,我们打开目录下的src
文件夹,会发现如下的几个文件
- applyMiddleWare.ts
- bindActionCreators.ts
- combineReducers.ts
- compose.ts
- createStroe.ts
- index.ts
我们需要着重于看标橙色的文件的实现。
首先我们来看看我们平常使用redux的时候会做什么:
- 初始化state,创建reducer函数
- 从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数据了。
这个疑问到这里就结束了,是不是很开心。
但是我们的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把数组中的这一项给移除掉了, 这样我们就可以达到取消订阅的效果拉~~
看到这里,大家会不会有一个疑问: 这个返回值这么多代码,就只讲这一句,你是不是不会啊?
- ensureCanMutateNextListeners有啥作用啊?
- 为啥会有currentListeners和nextListenrs啊?一个不行吗?
- isSubscribed是啥,也是状态锁吗?
- 。。。。
说真的,我之前真不会,不过之后自己也弄清楚了
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的元素,看看结果呢?
结果很显然,我们在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个函数
那么这个功能是怎么实现的呢? 那就要归功于 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("第二次执行完毕");
执行结果如下:
是不是突然就解决了这个疑问,是不是就知道了为啥==redux中嵌套订阅也不会有逻辑问题啦,就是浅拷贝的功劳!!!==
对于取消订阅越是一样的操作!
至于observable
和replaceReducer
我还没用过。。。(打脸了),之后再补上。
到这里 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后再看到这个,我会有几个疑问:
- state都在每个reducer里,怎么把对应的state全部整合到一个里面呢?
- 整合过后的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怎么合并到一起的==?是不是就解决啦~~
那么还剩下一个问题:==整合过后的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都执行后,看看全部的状态有没有发生变化。
我超,是不是很神奇~~~!!!
compose
本来是想先看applyMiddleWare的,但是发现applyMiddleWare中用到了compose去做一些链式的处理,所以得先学学compose
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)))
}
只是最后的一段代码,看到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。
那么现在就有一些疑问了:
- 为什么可以dispatch一个函数了呢?
- getData的返回函数的参数dispatch为什么才是真正的dispatch?
- 中间件触发的时机是什么时候
了解了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的原因:这里有几个疑问,
- 为什么dispatch被重写了,为什么middleWare里的dispatch是抛出错误的函数,而返回的dispatch并不是
- 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啊。
一个中间件就够绕了,多个中间件。。。。算了。。。我再学习学习再讲。。。。呜呜呜
自定义中间件
总结
- 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客
- 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客
- 、、、 如果感兴趣 请移步个人博客DingSHiYi的个人博客