Redux 源码解读 & 中间件机制

525 阅读7分钟

前言

目前公司是基于React的技术栈,而我自己对于React的理解只限于应付项目业务开发而已,而且公司项目基本都是依赖umi框架机制,导致新人甚至对一些最基本的知识点都不了解就开始着手开发,比如react-router/react-redux

大佬造的轮子用多了也是会有负面影响,为了不让自己做一条只会用别人东西的咸鱼,决定去了解下redux的原理,因为redux源码相对比较简洁而且通过redux大致都能了解其他的状态管理工具的设计思路,一通则百通,无外乎这个道理,以后就算有几百个轮子都可以很快理解

工作流程图

在了解源码之前先可以仔细看下Redux的整体流程图(感谢若川大佬的图,这里冒昧借用下),如果能简单看懂这张图,那么redux的源码学习将会非常快速

调试

为了了解源码,就必须要学会调试代码,现在我们去 redux@4.0.4 拷贝redux的代码到本地

git clone https://github.com/reduxjs/redux.git

可以很清晰的看到redux是使用rollup打包,为了能搞更好地调试,给它加上sourcemap配置

//packages.json
"scripts": {
   "build": "rollup -c -m",
   ...
}

执行yarn build命令,可以看到生成了es/dist/lib对应不同的环境

接下来我是直接使用的官方examples文件夹里的async例子,直接把es文件复制进去,在examples/async/index.js 里更改引入

// import { createStore, applyMiddleware } from 'redux'
import { createStore, applyMiddleware } from './es/redux.js'

源码理解

很多文章都是从createStore开始介绍,我按照自己的思路,从combineReducers开始一步一步理解redux的机制

combineReducers

先看下一个redux官方给出的简单例子: /reducesc/index.js

这里定义了postsBySubreddit,selectedSubreddit两个函数,即为两个reducer, 也是我们项目中都会存在的,而且只会比它多。而在index.js中,需要把整合的rducer传入createStore中,下面我们看看 combineReducers 对这两个reducer做了什么

可以很清晰得看到combineReducers获取两个reducer函数,经过简单地校验,生成了类似{ [function.name]: function }的键值对象, 并存放在闭包中(finalReducers), 最后返回了一个combination函数,用来传入createStore中作为形参,而combination具体做了什么下面会讲

createStore

reducer作为参数传入createStore中之后,createStore函数执行返回核心的store对象,顺着思路来看下createStore中做了什么

可以很清楚的看到store就是返回dispatchsubscribegetStatereplaceReducer等方法。工作中用到过redux的应该非常熟悉。

现在我们具体看下对传进来的reducer做了什么,其实上面都是一些方法的定义,重点要注意这句

// When a store is created, an "INIT" action is dispatched so that every
// reducer returns their initial state. This effectively populates
// the initial state tree.
dispatch({ type: ActionTypes.INIT })

这里单独执行一个distpatch方法,是为了初始化生成一个state tree, 也就是生成一个包含store里面所有state的树形结构对象,接下来主要看下dispatch做了什么, 而我们之前传入的reducer也就在这里起了作用

dispatch

这里我们需要关注的重点就是currentReducer(其实就是上文传过来的combination函数),这里调用了这个函数,并传入state/action

这里可以看出combination拿到最初的stateaction, 先做了简单的判断,之后也就是这里的一个问题:遍历执行所有的reducer来改变state

当然,最初的时候是形成了基本的树形结构state, 但是之后的每一步dispatch都要去遍历所有的reducer得到新的state进行比较再返回最终的state, 保存进createStore里的currentState中,可以通过store暴露的getState方法获取当前的state

subscribe

subscribe其实就是订阅发布模式的一种实现吧,我是这么理解的,每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。它的代码也很简单

我们在index.js中加入,然后开始调试,它会走到createStore中暴露的subscribe方法里

debugger;
store.subscribe(() => console.log(7))

subscribe方法会把listener加入到nextListeners中,相当于发布订阅模式中的消息队列,之后在dispatch函数执行的时候,遍历nextListeners出发listener函数,具体代码在dispatch中查看,这里不做详述了

以上是对基本的redux机制做了简单的分析理解,我们会发现这些功能还不足以完全承载我们的业务,所以redux本身引入中间件的概念对本身功能进行扩展。社区有很多中间件的开源库,包括redux-thunk,redux-promise,redux-saga等等等等,学是学不完的,但还是那句话,一通百通的道理......

中间件机制

applyMiddleware

如果我们要加入中间件,可以在createStore中加入参数applyMiddleware(...middlewares), 这里我以redux-thunk为例

import { createStore, applyMiddleware } from './es/redux.js'
import thunk from 'redux-thunk'
import reducer from './reducers'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

看一下applyMiddleware函数具体做了什么

这里applyMiddleware用了柯里化的理念,按上面的例子来说第一个参数就是thunk,获取数据之后会在createStore里调用

接下来看下createStore中对于中间件的处理逻辑

这里的enhancer函数就是上方的applyMiddleware(thunk)返回函数,再次传入createStore以及初始化的reducerpreloadedState, 这表示又再次走到了applyMiddleware函数里面

这里跟上面一样生成了store对象,赋给middleware返回chain,这里最难理解的就是compose方法,也是中间件机制的关键所在

compose

这里其实也是函数式编程的一个组合的理念,把函数从右至左进行组合,可以理解为上个函数的返回就是下个函数的参数,如果还是不明白这个reduce代码可能不理解,那我们慢慢来理解它

首先先看个例子:

很明显结果是108, 执行顺序依次是multiply,add,minus,那接下来我们把它写成通用的函数

redux中的compose跟这很类似,但是区别在于它返回的是双层函数,即next(...)函数,这里是中间件机制最难理解的地方,它就是通过这种方式来实现洋葱模型的

为了理解compose的组合机制,我们先回来看下react-thunk的代码

接来下我们再自己实现一个简单的middleware

在看下上面的调试的截图,多个中间件时,chain则等于[next => action => ..., next => action => ...], 再进入compose函数,得到的结构就是(...args) => a(b(...args)), 这时又传入store.dispatch(即next函数)

  1. 再观察下这两个中间件,store.dispatch传入之后,next被替代为store.dispatch 而返回了action => {}函数,这个函数又作为下一个中间件的next函数
  2. 当用户执行dispatch(action), 中间件接收到了action,则开始调用刚传入的 next函数指向外部next直至store.dispatch

现在我们根据redux-thunk具体的例子来理解:

export const requestPosts = subreddit => ({
  type: REQUEST_POSTS,
  subreddit
})

const fetchPosts = subreddit => (dispatch, getState) => {
  dispatch(requestPosts(subreddit))
}

// dispatch action in redux-thunk
dispatch(fetchPosts(subreddit))
  1. 根据react-thunk的代码,fetchPosts(subreddit)的返回值就是action, 而action是函数, 则return action(dispatch, getState, extraArgument);, 根据例子上的第二个返回函数(dispatch, getState) => {}由此而来

  2. 这里的action(dispatch, ...)中的dispatch则是compose组合返回的, requestPosts(subreddit)执行后再次进入react-thunk中间件,由于不是函数则执行next(action),这时候如果有其他的中间件,则根据compose继续向左执行

总结

通过以上对Redux源码的简单解析,大致理解了它本身的内部机制, 现在再到头部看那张流程图是否理解得更加透彻了呢

参考资料

Redux 官方文档

学习 redux 源码整体架构,深入理解 redux 及其中间件原理

redux-thunk