redux源码详解(一)
本次主要分享一下redux的源码解析,以及我在阅读源码的过程中遇到的一些问题。主要分为两部分,第一部分为实现基本的redux功能,主要包含两个js文件的分析,一个是用来生成store对象,另一个是用来实现reduce函数的拆分与合并。第二部分是实现异步的dispatch,主要是通过中间件函数来加强dispatch实现的。
一.实现基本的redux功能
我们先来讲第一部门,如何实现基本的redux功能。
下图是redux官网一张动图,解释了redux的基本运作原理。
先用createStore函数生成一个store对象,store是一个全局状态机,存放全局的状态state。添加订阅函数,可以在state对象发生变化的时候通知你。如果想改变其中某一个状态,用dispatch发出一个action,dispatch会自动执行reduce函数,该函数会在原来state的基础上重新计算并返回一个新的state对象,状态改变之后,通知所有的订阅函数,改变页面UI。
redux源码分为几个主要文件:
1.createStore.js文件:基本的redux文件,生成全局状态机
2.combineReducer.js文件:用于reducer的拆分
3.applyMiddleware.js文件:用于异步函数的实现
一.createStore.js文件
接下来,我们先来讲一下createStore的源码,该文件主要包含三个方法:
getState:用来获取store中的状态变量
dispatch:发出一个action,改变store中state的值
subscribe:订阅state更新的事件,状态改变时通知订阅事件
下面是createStore.js的源码
function createStore(reducer, preloadedState, enhancer){
// 增强dispatch函数 start
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
// 判断 enhancer 不是一个函数
if (typeof enhancer !== 'function') {
// 抛出一个异常 (enhancer 必须是一个函数)
throw new Error('Expected the enhancer to be a function.')
}
// 调用 enhancer ,返回一个增强版的 store creator
return enhancer(createStore)(reducer, preloadedState)
}
// 增强dispatch函数 end
let currentState = preloadedState // 当前的state
let isDispatching = false // 是否正在执行dispatch函数
let currentListeners = [] //当前的监听函数列表
let nextListeners = currentListeners // 新生成的监听函数列表
// 获取store中的变量值
function getState () {
if (isDispatching) {
throw new Error('currentState已经传输给reducer函数,不希望我们通过getState的方法获取state对象')
}
return currentState
}
/**
触发reducer函数改变state
通知所有的订阅函数
*/
function dispatch (action) {
try {
isDispatching = true
// 执行reduce函数,改变state,返回新的state对象
currentState = reducer(currentState, action)
} finally {
isDispatching = false
}
// state发生变化时,通知所有的订阅函数
let listeners = (currentListeners = nextListeners)
for(let i = 0;i<listeners.length;i++){
listeners[i]()
}
return action
}
/**
新增订阅函数
返回取消订阅函数
*/
function subscribe(listener){
let isSubscribed = true
// 保证 nextListeners 的变化不影响当前currentListeners
ensureCanMutateNextListeners()
// 添加新的监听函数到nextListeners
nextListeners.push(listener)
// 返回该订阅函数的取消订阅函数
return function unsubscribe(){
// 防止取消订阅两次
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index,1)
// TODO 为啥
currentListeners = null
}
}
// 保证 nextListeners 的变化不影响当前 currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 调用reducer初始化state对象
dispatch({})
return {
getState,
dispatch,
subscribe
}
}
复制代码
程序中主要的方法有三个,注释也记录的比较清晰,这里就不细讲了。
在分析源码以及重写源码的过程中,我遇到了几个问题,希望能给大家带来一点帮助:
问题1:可以不通过dispatch而直接修改store中state的值吗? 为什么不推荐这样做?
问题2:在dispatch的时候 为什么不能执行getState方法
问题3:为什么要用currentListeners和nextListeners(store.subscribe嵌套的情况)
问题4:为什么要在最后执行一次dispatch?将state初始化,使得state在初始值的情况下进行更新
问题一:
这个问题之前在项目中遇到过,印象比较深刻。由于getState方法中返回的是currentState本身,而不是它的拷贝,所以可以直接改变currentState对象。但是redux不推荐这样做,因为这样改变state之后,不会通知订阅函数。dispatch是唯一推荐改变state的方式。
问题二:
currentState已经传输给reducer函数,不希望我们通过getState的方法获取state对象
问题三:
问题可以总结为:dispatch时将nextlisteners赋值给currentListeners,并执行currentListeners中的每一个订阅函数。subscribe时nextListeners用来添加删除listener,并确保nextListeners的增删不会影响currentListeners。
既然currentListeners只用到了一处,并且还等于nextlisteners。为什么不能只用一个?
我发现只用nextListeners不用currentListeners,也可以实现基本的功能。但是当订阅函数相互嵌套时,就会出现不一致的情况。我们来举例说明:
上面的程序当中,订阅函数里面又嵌套了一个订阅函数,这意味着当遍历currentLIsteners数组执行订阅函数时,又会添加一个新的订阅函数到nextListeners数组中,造成结果输出不一致的情况出现。
通过上图可以发现,两者是同一个引用时、两者引用不同时,得到的打印结果不一样。主要区别在于dispatch方法执行时(遍历currentLIsteners)添加了新的listeners到nextListeners中,这个时候如果currentLIsteners和nextListeners是同一个引用时,相当于遍历currentLIsteners事件的同时,添加了一个新的事件到currentLIsteners中。
所以,要用两个订阅事件集合。我们不希望在dispatch的时候改变订阅函数集。
问题四:
store创建好之后,立即发出一个初始化action,是为了让reducer返回store的初始化状态,否则,创建store之后,调用getState方法得到的就是undefined。
二 .combineReducers.js方法
reducer函数负责生成state,整个应用只有一个state对象,包含所有的数据,对于一个大型应用来说,state必然比较庞大,会导致reducer函数也十分庞大,所以需要对reducer函数进行拆分。并且几个人开发的项目中,我们需要编写各自的reucer函数并合并在一起,因为createStore只能接收一个reducer函数。这就是combineReducers函数做的事情。下面来举例说明:
假设我们项目当中有两个reducer函数,reducerI 和 reducerII 函数,将两个reducer函数以对象的形式传入combinereducers函数,函数执行返回的finalReducer作为reducer函数传给createStore,实现了原本reducer函数相同的功能。
假设我们项目当中有两个reducer函数,reducerI 和 reducerII 函数,分别负责项目中不同的模块,由于传入到createStore中的reducer函数只有一个,所以我们需要将两个reducer函数合并成一个。
将两个reducer函数传入combinereducers函数,返回一个合成的reducer函数,就可以实现一样的redux功能。
运行结果:
发现:combineReducers实现了合并reducer函数的功能,我们好奇的是,combineReducers是如何通过传入的action来改变其对应的state,并将state合并成一个对象输出的。接下来,我们来看combineReducers.js源码:
// 参数reducers是一个对象,key值和state的key值一样,value是一个reducer函数
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
// 遍历key值,过滤掉value不是reducer函数的值
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
// 过滤掉value不是reducer函数的值
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// 返回一个合成的reducer函数(接收state和action作为参数)
return function combination(state = {}, action) {
let hasChanged = false // 记录state对象是否改变的变量
const nextState = {} // 下一个状态
// 遍历key值
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key] // 当前key值对应的状态
const nextStateForKey = reducer(previousStateForKey, action) //调用reducer后新生成的状态
nextState[key] = nextStateForKey //将新生成的状态赋值到新状态的对应key值上
// 通过对比previousStateForKey 和 nextStateForKey是否相等来表明状态是否改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 如果状态改变就返回新的状态,没有,就返回原来的state对象
return hasChanged ? nextState : state
}
}
// combineReducers函数实际上也是一个reducer函数,接收state和action作为参数,返回一个新的state
复制代码
combineReducers接收reducers对象为入参,返回一个合成的reducer函数,利用闭包,实现了reducer合成的功能。实质是通过遍历的形式,将action传入每一个reducer函数,获取初始值或者改变state,返回新的state对象,与普通的reducer函数很相像。
三. applyMiddleware.js文件
在一个项目当中,如果在两个文件中需要请求相同的数据,以往的做法是分别在两个文件中请求,得到数据之后再分别dispatch进行数据保存。这样请求数据的函数其实写了两遍,redux将异步函数的执行封装在了dispatch方法内部,用redux实现异步dispatch之后,只需要在两个文件当中dispatch调用该异步方法即可。
从一开始的dispatch只能接收对象,到现在的dispatch可以执行异步函数,其实是redux用中间件函数增强了dispatch,让dispatch做了更多的事情。
如何能让reducer函数在异步函数结束后自动执行?这就需要用到中间件(middleware)来增强dispatch函数。
下面,我们来举例说明:
运行结果:
上述程序运行以及结果说明,加入中间件thunk之后,我们dispatch一个异步的操作,可以在异步操作执行结束之后再执行reducer函数,达到了我们想要的目标。
下面我们来分析一下applyMiddleware.js的源码,搞明白中间件是怎么实现异步操作的。
为了搞明白applyMiddleware.js,我们需要提前了解compose函数和reduce函数的源码:
1)reduce函数的源码
Array.prototype.myReduce = function (fn, init) {
//数组的长度
var len = this.length;
var pre = init;
var i = 0;
//判断是否传入初始值
if (init == undefined) {
//没有传入初始值,数组第一位默认为初始值,当前元素索引值变为1。
pre = this[0];
i = 1;
}
for (i; i < len; i++) {
//当前函数返回值为下一次的初始值
pre = fn(pre, this[i], i)
}
return pre;
}
复制代码
var arr = [1, 2, 5, 4, 3];
var add = arr.myReduce(function (preTotal, ele, index) {
return preTotal + ele;
}, 100)
console.log(add);//115
复制代码
是我们常见的用reduce函数实现的数组迭代求和的例子,在数组上调用reduce,reduce接收的入参是函数fn和数组迭代初始值100,第一次函数的执行是取初始值100和数组第一项1相加,将函数执行的结果101作为函数的第一个入参,并取数组第二项2作为函数fn的第二个入参,第二次执行函数fn,如此迭代下去,直到返回整个数组和初始值相加的结果115。
在数组上执行reduce函数,实际上是迭代数组的每一项,将结果作为入参返回给下一次执行的函数
2)compose函数的源码
function compose() {
// 将arguments的每一项拷贝到func中去
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 () {
// arguments是返回函数的入参
return a(b.apply(undefined, arguments));
};
});
}
复制代码
下面,我们举例说明compose函数的作用:
const value = compose(function (value) {
return value + 1;
}, function (value) {
return value * 2;
}, function (value) {
return value - 3;
})(2); //-1
复制代码
将f1 f2 f3 三个函数依次传入compose函数,compose函数的精髓在于reduce函数,其实就是将上一次函数执行的值作为下一次函数的入参。下图是将参数依次带入reduce函数得到的执行结果。可以发现,传入compose的函数数组是按照从右往左的顺序依次执行的。(洋葱函数)
3)applyMiddleware.js函数的源码
在熟悉了上面两个函数之后,我们就来啃最后一根最难啃的骨头,即中间件函数。中间件函数是在哪里用的呢,它是作为第三个参数传入createStore函数中的,接下来我们先完善createStore,看看它在传入中间件函数时是怎么处理的。
createStore.js 源码:
function createStore(reducer, preloadedState, enhancer){
// 增强dispatch函数 start
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
//判断 enhancer 不是一个函数
if (typeof enhancer !== 'function') {
//抛出一个异常 (enhancer 必须是一个函数)
throw new Error('Expected the enhancer to be a function.')
}
//调用 enhancer ,返回一个新强化过的 store creator
return enhancer(createStore)(reducer, preloadedState)
}
// 增强dispatch函数 end
// 确保 nextListeners 与 currentListeners 保持一致,且保证 nextListeners 的变化不影响当前 currentListeners
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
let currentState = preloadedState // 当前的state
let isDispatching = false // 是否正在执行dispatch
let currentReducer = reducer // 当前的reducer
let currentListeners = [] //当前的监听函数列表
let nextListeners = currentListeners // 新生成的监听函数列表
function getState () {
// 正在执行 dispatch 中,不能调用 store.getState()
if (isDispatching) {
throw new Error('改变数据的同时不能获取数据')
}
return currentState
}
/**
* 核心函数:触发 state 改变的唯一方法
* 当发送 dispatch(aciton) 时,用于创建的 store 的 ‘reducer(纯函数)’ 都会被调用一次。调用的传入的参数是当前的 state 和发送 ‘action’,
* 调用完成后,所有的 state 监听函数都会触发。
*/
function dispatch (action) {
if (isDispatching) {
throw new Error('不能同时改变数据')
}
try {
isDispatching = true
// 执行reduce函数,改变state,返回新的state对象
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// state发生变化时,通知所有的订阅函数
let listeners = (currentListeners = nextListeners)
for(let i = 0;i<listeners.length;i++){
listeners[i]()
}
return action
}
// 新增一个变更监听函数且返回一个解绑函数,作用于 dispatch 内部执行,每当dispatch(action)后所有监听函数都会被触发一次
function subscribe(listener){
if (isDispatching) {
throw new Error('改变数据的同时不能订阅事件')
}
let isSubscribed = true
ensureCanMutateNextListeners()
// 添加新的监听函数到nextListeners
nextListeners.push(listener)
// 返回该订阅函数的取消订阅函数
return function unsubscribe(){
if (!isSubscribed) {
return
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index,1)
}
}
// 初始化state对象
// dispatch({})
return {
getState,
dispatch,
subscribe
}
}
复制代码
上面是加入enhancer入参之后的程序,其中最主要的一行代码就是:
return enhancer(createStore)(reducer, preloadedState)
复制代码
现在,我们贴上applyMiddleware.js的代码进行对比,看看中间件到底做了什么?
// enhancer(createStore)(reducer, preloadedState) = applyMiddleware(...middlewares)
function applyMiddleware(...middlewares) {
// applyMiddleware返回一个enhancer函数
return (createStore)=> {
//enhancer函数返回一个生成store的函数
return (...args) => {
//生成基本的store函数
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)
}
//将middlewareAPI 注入到中间件 得到中间件函数第二层函数的数组
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
}
复制代码
对比之前的异步执行程序:
对比上图中的程序可以发现,applyMiddleware函数返回的是一个enhancer函数,enhancer函数接受一个createStore函数作为入参,enhancer函数执行后返回的函数接收reducer、preloadedState作为入参,该函数执行之后,返回的是一个具有加强版disptach方法的store对象。我们再来看applyMiddleware函数的下半部分:
上图中,做的事情主要就是生成基本的store对象,利用中间件对dispatch函数进行增强,返回一个新的store对象。那,中间件函数是如何对dispatch函数进行加强的呢?我们用两个中间件函数进行举例:
thunk函数和logger函数是react-redux封装的两个比较常用的中间件函数,分别用来处理异步操作和打印state,他们都有统一的格式:({dispatch,getState}) =>{return next => action => {}}
applyMiddleware得到的chain数组,就是将dispatch和getState注入到中间件函数得到的第二层函数数组,数组的形式为:next => action => {}
下面的f1 f2函数是将中间件入参传入到中间件函数之后得到的chain数组中的函数
接下来,到最重要的一步:
执行代码过后,将f1 f2函数带入到代码当中,可以得到新的dispatch为:
可以发现dispatch真的被增强了,不仅可以实现异步,还可以打印dispatch前后的state。
我们用新的dispatch执行异步函数,程序判断action是一个函数,进入到上半部分,开始执行异步函数,setTimeout执行过后,又dispatch一次,又进入到下半部分,开始打印dispatch之前的state,然后调用store.dispatch(原来的dispatch)更新state,最后打印dispatch之后的state。
最后的执行结果是:
可以发现dispatch真的被增强了,不仅可以实现异步,还可以打印dispatch前后的state。
到底为止,我们就解析结束了redux的源码,希望能给大家带来一丝帮助。