共计 27171 个字符,预计需要花费 68 分钟才能阅读完成。
前言
用过 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 方法主要检测两点:
不能占用 <redux/*> 的命名空间
如果遇到未知的 action 的类型,不需要要用默认返回值
如果传入 type 为 @@redux/INIT< 随机值 > 的 action,返回 undefined,说明没有对未知的 action 的类型做响应,需要加默认值。如果对应 type 为 @@redux/INIT< 随机值 > 的 action 返回不为 undefined, 但是却对应 type 为 @@redux/PROBE_UNKNOWN_ACTION_< 随机值 > 返回为 undefined,说明占用了 <redux/*> 命名空间。整个逻辑相对简单,好好自己梳理一下。
第三步:返回一个函数,用于代理所有的 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 代码量不是很大,但是里面的东西还是很多的,逻辑相对来说有点绕。不过没关系,没有什么是看了好几次都看不懂的,如果有那就再多看几次嘛!另外再多一嘴,如果想快读提高自己的小伙伴们,我个人是强烈推荐看源码的。正所谓“近朱者赤,近墨者黑”,多看看大神的代码,对自己的代码书写、代码逻辑、知识点查缺补漏等等方面都是很大帮助的。就拿我自己来说,我每次阅读完一篇源码之后,都受益匪浅。可能第一次看源码,有着诸多的不适应,毕竟万事开头难,如果强迫自己完成第一次的源码阅读,那往后的源码阅读将会越来越轻松,对自己的提升也就越来越快。各位骚年们,撸起袖子加油干吧!