Open MrErHu opened 7 years ago
我看到combineReducers这个方法里面,做了两次那reducers的key的操作,还有把reducer放到函数里面的一个变量里面去。其实为什么不直接就遍历reducers就行呢,照理应该也不会有改变值的操作什么的,是不是因为assertReducerShape
这个方法的原因呢
@TaiKyo 你说应该就是在一次循环既赋值又判断assertReducerShape
的逻辑吧。
想来是可以一次处理的,我猜测(估计)作者还是想把两个无关的逻辑分离吧。
接触Redux不过短短半年,从开始看官方文档的一头雾水,到渐渐已经理解了Redux到底是在做什么,但是绝大数场景下Redux都是配合React一同使用的,因而会引入了React-Redux库,但是正是因为React-Redux库封装了大量方法,使得我们对Redux的理解变的开始模糊。这篇文章将会在Redux源码的角度分析Redux,希望你在阅读之前有部分Redux的基础。
上图是Redux的流程图,具体的不做介绍,不了解的同学可以查阅一下Redux的官方文档。写的非常详细。下面的代码结构为Redux的master分支:
├── applyMiddleware.js ├── bindActionCreators.js ├── combineReducers.js ├── compose.js ├── createStore.js ├── index.js └── utils └── warning.js
Redux中src文件夹下目录如上所示,文件名基本就是对应我们所熟悉的Redux的API,首先看一下index.js中的代码:
上面的代码非常的简单了,只不过是把所有的方法对外导出。其中
isCrushed
是用来检查函数名是否已经被压缩(minification)。如果函数当前不是在生产环境中并且函数名被压缩了,就提示用户。process是Node 应用自带的一个全局变量,可以获取当前进程的若干信息。在许多前端库中,经常会使用 process.env.NODE_ENV这个环境变量来判断当前是在开发环境还是生产环境中。这个小例子我们可以get到一个hack的方法,如果判断一个js函数名时候被压缩呢?我们可以先预定义一个虚函数(虽然JavaScript中没有虚函数一说,这里的虚函数(dummy function)指代的是没有函数体的函数),然后判断执行时的函数名是否和预定义的一样,就像上面的代码:compose
从易到难,我们在看一个稍微简单的对外方法
compose
理解这个函数之前我们首先看一下
reduce
方法,这个方法我是看了好多遍现在仍然是印象模糊,虽然之前介绍过reduce
,但是还是再次回忆一下Array.prototye.reduce
:reduce()
函数对一个累加值和数组中的每一个元素(从左到右)应用一个函数,将其reduce
成一个单值,例如:reduce()
函数接受两个参数:一个回调函数和初始值,回调函数会被从左到右应用到数组的每一个元素,其中回调函数的定义是现在回头看看
compose
函数都在做什么,compose
函数从左到右组合(compose)多个单参函数。最右边的函数可以按照定义接受多个参数,如果compose
的参数为空,则返回一个空函数。如果参数长度为1,则返回函数本身。如果函数的参数为数组,这时候我们返回我们知道
reduce
函数返回是一个值。上面函数传入的回调函数是(a, b) => (...args) => a(b(...args))
其中a
是当前的累积值,b
是数组中当前遍历的值。假设调用函数的方式是compose(f,g,h)
,首先第一次执行回调函数时,a
的实参是函数f
,b
的实参是g
,第二次调用的是,a
的实参是(...args) => f(g(...args))
,b
的实参是h
,最后函数返回的是(...args) =>x(h(...args))
,其中x为(...args) => f(g(...args))
,所以我们最后可以推导出运行compose(f,g,h)
的结果是(...args) => f(g(h(...args)))
。发现了没有,这里其实通过reduce
实现了reduceRight
的从右到左遍历的功能,但是却使得代码相对较难理解。在Redux 1.0.1版本中compose
的实现如下:这样看起来是不是更容易理解
compose
函数的功能。bindActionCreators
bindActionCreators
也是Redux中非常常见的API,主要实现的就是将ActionCreator
与dispatch
进行绑定,看一下官方的解释:翻译过来就是
bindActionCreators
将值为actionCreator
的对象转化成具有相同键值的对象,但是每一个actionCreator
都会被dispatch
所包裹调用,因此可以直接使用。话不多说,来看看它是怎么实现的:对于处理单个
actionCreator
的方法是代码也是非常的简单,无非是返回一个新的函数,该函数调用时会将
actionCreator
返回的纯对象进行dispatch
。而对于函数bindActionCreators
首先会判断actionCreators
是不是函数,如果是函数就直接调用bindActionCreator
。当actionCreators
不是对象时会抛出错误。接下来:这段代码也是非常简单,甚至我觉得我都能写出来,无非就是对对象
actionCreators
中的所有值调用bindActionCreator
,然后返回新的对象。恭喜你,又解锁了一个文件~applyMiddleware
applyMiddleware
是Redux Middleware的一个重要API,这个部分代码已经不需要再次解释了,没有看过的同学戳这里Redux:Middleware你咋就这么难,里面有详细的介绍。createStore
createStore
作为Redux的核心API,其作用就是生成一个应用唯一的store。其函数的签名为:前两个参数非常熟悉,
reducer
是处理的reducer
纯函数,preloadedState
是初始状态,而enhancer
使用相对较少,enhancer
是一个高阶函数,用来对原始的createStore
的功能进行增强。具体我们可以看一下源码:具体代码如下:
我们来逐步解读一下:
我们发现如果没有传入参数
enhancer
,并且preloadedState
的值又是一个函数的话,createStore
会认为你省略了preloadedState
,因此第二个参数就是enhancer
。如果你传入了
enhancer
但是却又不是函数类型。会抛出错误。如果传入的reducer
也不是函数,抛出相关错误。接下来才是createStore
重点,初始化:currentReducer
是用来存储当前的reducer
函数。currentState
用来存储当前store中的数据,初始化为默认的preloadedState
,currentListeners
用来存储当前的监听者。而isDispatching
用来当前是否属于正在处理dispatch
的阶段。然后函数声明了一系列函数,最后返回了:显然可以看出来返回来的函数就是
store
。比如我们可以调用store.dispatch
。让我们依次看看各个函数在做什么。dispatch
我们看看
dispath
做了什么,首先检查传入的action
是不是纯对象,如果不是则抛出异常。然后检测,action
中是否存在type
,不存在也给出相应的错误提示。然后判断isDispatching
是否为true
,主要是预防的是在reducer
中做dispatch
操作,如果在reduder
中做了dispatch
,而dispatch
又必然会导致reducer
的调用,就会造成死循环。然后我们将isDispatching
置为true
,调用当前的reducer
函数,并且返回新的state
存入currentState
,并将isDispatching
置回去。最后依次调用监听者store
已经发生了变化,但是我们并没有将新的store
作为参数传递给监听者,因为我们知道监听者函数内部可以通过调用唯一获取store
的函数store.getState()
获取最新的store
。getState
实在太简单了,自行体会。
replaceReducer
replaceReducer
的使用相对也是非常少的,主要用户热更新reducer
。subscribe
subscribe
用来订阅store
变化的函数。首先判断传入的listener
是否是函数。然后又调用了ensureCanMutateNextListeners
,可以看到
ensureCanMutateNextListeners
用来判断nextListeners
和currentListeners
是否是完全相同,如果相同(===
),将nextListeners
赋值为currentListeners
的拷贝(值相同,但不是同一个数组),然后将当前的监听函数传入nextListeners
。最后返回一个unsubscribe
函数用来移除当前监听者函数。需要注意的是,isSubscribed
是以闭包的形式判断当前监听者函数是否在监听,从而保证只有第一次调用unsubscribe
才是有效的。但是为什么会存在nextListeners
呢?首先可以在任何时间点添加
listener
。无论是dispatch
action时,还是state
值正在发生改变的时候。但是需要注意的,在每一次调用dispatch
之前,订阅者仅仅只是一份快照(snapshot),如果是在listeners
被调用期间发生订阅(subscribe)或者解除订阅(unsubscribe),在本次通知中并不会立即生效,而是在下次中生效。因此添加的过程是在nextListeners
中添加的订阅者,而不是直接添加到currentListeners
。然后在每一次调用dispatch
的时候都会做:来同步
currentListeners
和nextListeners
。observable
该部分不属于本次文章讲解到的内容,主要涉及到RxJS和响应异步Action。以后有机会(主要是我自己搞明白了),会单独讲解。
combineReducers
combineReducers
的主要作用就是将大的reducer
函数拆分成一个个小的reducer
分别处理,看一下它是如何实现的:首先,通过一个
for
循环去遍历参数reducers
,将对应值为函数的属性赋值到finalReducers
。然后声明变量unexpectedKeyCache
,如果在非生产环境,会将其初始化为{}
。然后执行assertReducerShape(finalReducers)
,如果抛出异常会将错误信息存储在shapeAssertionError
。我们看一下shapeAssertionError
在做什么?可以看出
assertReducerShape
函数的主要作用就是判断reducers
中的每一个reducer
在action
为{ type: ActionTypes.INIT }
时是否有初始值,如果没有则会抛出异常。并且会对reduer
执行一次随机的action
,如果没有返回,则抛出错误,告知你不要处理redux中的私有的action,对于未知的action应当返回当前的stat。并且初始值不能为undefined
但是可以是null
。接着我们看到
combineReducers
返回了一个combineReducers
函数:在
combination
函数中我们首先对shapeAssertionError
中可能存在的异常进行处理。接着,如果是在开发环境下,会执行getUnexpectedStateShapeWarningMessage
,看看getUnexpectedStateShapeWarningMessage
是如何定义的:我们简要地看看
getUnexpectedStateShapeWarningMessage
处理了哪几种问题:然后
combination
执行其核心部分代码:使用变量
nextState
记录本次执行reducer
返回的state。hasChanged
用来记录前后state
是否发生改变。循环遍历reducers
,将对应的store
的部分交给相关的reducer
处理,当然对应各个reducer
返回的新的state
仍然不可以是undefined
。最后根据hasChanged
是否改变来决定返回nextState
还是state
,这样就保证了在不变的情况下仍然返回的是同一个对象。最后,其实我们发现Redux的源码非常的精炼,也并不复杂,但是Dan Abramov能从Flux的思想演变到现在的Redux思想也是非常不易,希望此篇文章使得你对Redux有更深的理解。