上次我们已经写了 实现一个迷你Redux(基础版) ,这次我们来继续完善Redux,继续上篇的例子续写。
中间件
Redux 有个 API 是 applyMiddleware, 专门用来使用中间件的,首先我们得知道,它用来干嘛的。
为什么会需要中间件?
假设我们现在需要记录每次的 dispatch 前后 state 的记录, 那要怎么做呢?于是,简单粗暴的在第一个 dispatch 方法前后加代码
console.log('prev state', store.getState())
console.log(action)
store.dispatch({ type: 'INCREMENT' })
console.log('next state', store.getState())
这部分运行结果:
prev state {value: 10}
{type: "INCREMENT"}
当前数字为:11
next state {value: 11}
但加完发现情况不对,页面有多个 dispatch 的话,要这样写很多次,会产生大量重复代码。突然,又要加需求了,需要记录每次出错的原因,单独的功能要求如下:
try{
store.dispatch(action)
}catch(err){
console.error('错误信息: ', err)
}
然后两个需求都要,那就凑合两个,但叠一起看更乱了。
中间件的概念
显然,我们不能通过这种方式来做。比较理想的方案是Redux本身提供一个功能入口,让我们可以在外面添加功能进去,这样代码就不会复杂。
但如果给我们现有实现的Redux添加功能,在哪个环节添加比较合适呢?
- Reducer: 纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
- View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
- Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
我们发现,以上需求都是和 dispatch 相关,只有发送 action 的这个步骤,即 store.dispatch() 方法,可以添加功能。比如添加日志功能,我们只要把日志放进 dispatch 函数里,不就好了吗,我们只需要改造 dispatch 函数,把 dispatch 进行一层封装。
const store = createStore(counter)
const next = store.dispatch
store.dispatch = (action) => {
try{
console.log('prev state', store.getState())
console.log(action)
next(action)
console.log('next state', store.getState())
}catch(err){
console.error('错误信息: ', err)
}
}
上面代码,对 store.dispatch 进行了重新定义,这就是中间件的雏形。
所以说Redux的中间件就是一个函数,是对 dispatch 方法的扩展,增强 dispatch 的功能。
实现中间件
对于上述 dispatch 的封装,实际上是缺陷很大的。万一又来 n 多个需求怎么办? 那 dispatch 函数就混乱到无法维护了,故需要扩展性强的多中间件合作模式。
- 我们把 loggerMiddleware 提取出来
const store = createStore(counter)
const next = store.dispatch
const loggerMiddleware = (action) => {
console.log('prev state', store.getState())
console.log(action)
next(action)
console.log('next state', store.getState())
}
store.dispatch = (action) => {
try {
loggerMiddleware(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
- 把 exceptionMiddleware 提取出来
const exceptionMiddleware = (action) => {
try {
loggerMiddleware(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
store.dispatch = exceptionMiddleware
- 现在代码有个问题,就是 exceptionMiddleware 中间件写死 loggerMiddleware,但以后又万一不要记录功能呢,所以我们需要让 next(action) 变成动态的,即换哪个中间件都可以
const exceptionMiddleware = (next) => (action) => {
try {
// loggerMiddleware(action)
next(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
这个写法可能刚开始看不太适应,实际就是函数里面,返回一个函数,即等效于
const exceptionMiddleware = function (next) {
return function (action) {
try {
// loggerMiddleware(action)
next(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
}
传参数的时候即是exceptionMiddleware(next)(action)
- 同理,我们让 loggerMiddleware 里面无法扩展别的中间件了!我们也把 next 写成动态的
const loggerMiddleware = (next) => (action) => {
console.log('prev state', store.getState())
console.log(action)
next(action)
console.log('next state', store.getState())
}
目前为止,整个中间件设计改造如下:
const store = createStore(counter)
const next = store.dispatch
const loggerMiddleware = (next) => (action) => {
console.log('prev state', store.getState())
console.log(action)
next(action)
console.log('next state', store.getState())
}
const exceptionMiddleware = (next) => (action) => {
try {
next(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
store.dispatch = exceptionMiddleware(loggerMiddleware(next))
- 现在又有一个新问题,想想平时使用中间件是从外部引入的,那外部中间件里面怎么会有 store.getState() 这个方法,于是我们把 store 也给独立出去。
const store = createStore(counter)
const next = store.dispatch
const loggerMiddleware = (store) => (next) => (action) => {
console.log('prev state', store.getState())
console.log(action)
next(action)
console.log('next state', store.getState())
}
const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action)
} catch (err) {
console.error('错误信息: ', err)
}
}
const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(logger(next))
- 如果又有一个新需求,需要在打印日志前输出当前时间戳,我们又需要构造一个中间件
const timeMiddleware = (store) => (next) => (action) => {
console.log('time', new Date().getTime())
next(action)
}
const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
const time = timeMiddleware(store)
store.dispatch = exception(time(logger(next)))
中间件使用方式优化
上面的写法可知,中间件的使用方式有点繁琐,故我们需要把细节封装起来,通过扩展createStore来实现。
先来看看期望的用法:
/* 接收旧的 createStore,返回新的 createStore */
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
/* 返回了一个 dispatch 被重写过的 store */
const store = newCreateStore(reducer);
实现 applyMiddleware
export const applyMiddleware = function (...middlewares) {
/* 返回一个重写createStore的方法 */
return function rewriteCreateStoreFunc(oldCreateStore) {
/* 返回重写后新的 createStore */
return function newCreateStore(reducer, preloadedState) {
// 生成 store
const store = oldCreateStore(reducer, preloadedState)
let dispatch = store.dispatch
// 只暴露 store 部分给中间件用的API,而不传入整个store
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => store.dispatch(action),
}
// 给每个中间件传入API
// 相当于 const logger = loggerMiddleware(store),即 const logger = loggerMiddleware({ getState, dispatch })
// const chain = [exception, time, logger]
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
// 实现 exception(time((logger(dispatch))))
chain.reverse().map((middleware) => {
dispatch = middleware(dispatch)
})
// 重写dispatch
store.dispatch = dispatch
return store
}
}
}
我们来看这一处代码:
chain.reverse().map((middleware) => {
dispatch = middleware(dispatch)
})
要注意一点,中间件是顺序执行,但是 dispatch 却是反序生成的。所以在这步会把数组顺序给反序(比如 applyMiddleware(A, B, C),因为 A 在调用时需要知道 B 的 dispatch,B 在执行时需要知道 C 的 dispatch,那么需要先知道 C 的 dispatch。)
官方Redux源码,采用了 compose 函数,我们也试试这种方式来写:
export const applyMiddleware = (...middlewares) => {
return (createStore) => (...args) => {
// ...
dispatch = compose(...chain)(store.dispatch)
// ...
}
}
// compose(fn1, fn2, fn3)
// fn1(fn2(fn3))
// 从右到左来组合多个函数: 从右到左把接收到的函数合成后的最终函数
export const compose = (...funcs) => {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
我们再对代码精简:
export const applyMiddleware = (...middlewares) => {
return (createStore) => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action),
}
const chain = middlewares.map((middleware) => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch,
}
}
}
export const compose = (...funcs) => {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
createStore的处理
现在的问题是,有两个 createStore 了,这怎么区分,上篇我们其实已经先告知了对中间件代码处理,但具体怎么推出的,我们继续看。
// 没有中间件的 createStore
const store = createStore(counter)
// 有中间件的 createStore
const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);
const newCreateStore = rewriteCreateStoreFunc(createStore);
const store = newCreateStore(counter, preloadedState);
为了让用户用起来统一一些,我们可以很简单的使他们的使用方式一致,我们修改下 createStore 方法
const createStore = (reducer, preloadedState, rewriteCreateStoreFunc) => {
// 如果有 rewriteCreateStoreFunc,那就采用新的 createStore
if(rewriteCreateStoreFunc){
const newCreateStore = rewriteCreateStoreFunc(createStore);
return newCreateStore(reducer, preloadedState);
}
// ...
}
不过Redux源码 rewriteCreateStoreFunc 换了个名字,还加了判断,也就是我们上篇的代码:
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
所以中间件的用法为
const store = createStore(counter, /* preloadedState可选 */ applyMiddleware(logger))
combineReducers
如果我们做的项目很大,有大量 state,那么维护起来很麻烦。Redux 提供了 combineReducers 这个方法,作用是把多个 reducer 合并成一个 reducer, 每个 reducer 负责独立的模块。
我们用一个新例子来举例:
import { createStore, applyMiddleware, combineReducers } from 'redux'
const initCounterState = {
value: 10,
}
const initInfoState = {
name: 'jacky',
}
const reducer = combineReducers({
counter: counterReducer,
info: infoReducer,
})
// counter reducer处理函数
function counterReducer(state = initCounterState, action) {
switch (action.type) {
case 'INCREMENT':
return {
...state,
value: state.value + 1,
}
case 'DECREMENT':
return {
...state,
value: state.value - 1,
}
default:
return state
}
}
function infoReducer(state = initInfoState, action) {
switch (action.type) {
case 'FULL_NAME':
return {
...state,
name: state.name + ' lin',
}
default:
return state
}
}
const store = createStore(reducer)
const init = store.getState()
// 一开始counter为:10,info为 jacky
console.log(`一开始counter为:${init.counter.value},info为 ${init.info.name}`)
function listener() {
store.getState()
}
store.subscribe(listener) // 监听state的改变
// counterReducer
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
// infoReducer
store.dispatch({ type: 'FULL_NAME' })
// 执行完counter为:11,info为jacky lin
console.log(`执行完counter为:${store.getState().counter.value},info为${store.getState().info.name}`)
export default store
我们来尝试下如何实现这个 API,
首先要把一个函数里的所有 reducers 循环执行一遍,并且这个函数要遵循(state, action) => newState 格式。还需要把每个 reducer 的 initState 合并成一个 rootState。
实现如下:
export function combineReducers(reducers) {
// reducerKeys = ['counter', 'info']
const reducerKeys = Object.keys(reducers)
// 返回合并后的新的reducer函数
return function combination(state = {}, action) {
// 生成的新的state
const nextState = {}
// 遍历执行所有的reducers,整合成为一个新的state
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
const reducer = reducers[key]
// 之前的 key 的 state
const previousStateForKey = state[key]
// 执行 分 reducer,获得新的state
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
}
return nextState
}
}
replaceReducer
在大型 Web 应用程序中,通常需要将应用程序代码拆分为多个可以按需加载的 JS 包。 这种称为“代码分割”的策略通过减小初次加载时的 JS 的包的大小,来提高应用程序的性能。
reducer 拆分后,和组件是一一对应的。我们就希望在做按需加载的时候,reducer 也可以跟着组件在必要的时候再加载,然后用新的 reducer 替换老的 reducer。但实际上只有一个 root reducer 函数, 如果要实现的话就可以用 replaceReducer 这个函数,实现如下:
const createStore = function (reducer, initState) {
// ...
const replaceReducer = (nextReducer) => {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
reducer = nextReducer
// 刷新一遍 state 的值,新来的 reducer 把自己的默认状态放到 state 树上去
dispatch({ type: Symbol() })
}
// ...
return {
// ...
replaceReducer
}
}
使用如下:
const reducer = combineReducers({
counter: counterReducer
});
const store = createStore(reducer);
/*生成新的reducer*/
const nextReducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
/*replaceReducer*/
store.replaceReducer(nextReducer);
bindActionCreators
bindActionCreators 一般比较少用到,在react-redux的 connect 函数实现会用到
会使用到 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。
我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试
const reducer = combineReducers({
counter: counterReducer,
info: infoReducer
});
const store = createStore(reducer);
// 返回 action 的函数就叫 actionCreator
function increment() {
return {
type: 'INCREMENT'
}
}
function getName() {
return {
type: 'FULL_NAME',
}
}
const actions = {
increment: function () {
return store.dispatch(increment.apply(this, arguments))
},
getName: function () {
return store.dispatch(getName.apply(this, arguments))
}
}
// 其他地方在实现自增的时候,根本不知道 dispatch,actionCreator等细节
actions.increment(); // 自增
actions.getName(); // 获得全名
把actions生成时候的公共代码提取出来:
const actions = bindActionCreators({ increment, getName }, store.dispatch)
bindActionCreators 的实现如下:
// 返回包裹 dispatch 的函数, 将 actionCreator 转化成 dispatch 形式
// eg. { addNumAction } => (...args) => dispatch(addNumAction(args))
export function bindActionCreator(actionCreator, dispatch) {
return function (...args) {
return dispatch(actionCreator.apply(this, args))
}
}
export function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
// actionCreators 必须是 function 或者 object
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
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
}
可能大家看到这里有点懵逼,让我们来回忆下 react-redux 中 connect 函数的用法,
比如有这样一个 actionCreators
// actionCreators.js
function addNumAction() {
return { type: 'ADD_NUM' }
}
// Demo.js:在需要用到 store 数据的组件,如 Demo 组件底部我们用 connect 函数连接,如下:
import { addNumAction } from './actionCreators'
const mapDispatchToProps = (dispatch) => ({
addNum() {
dispatch(addNumAction())
}
})
export default connect(mapStateToProps, mapDispatchToProps)(Demo)
然后通过页面的按钮来出发 action 为 ADD_NUM 对应事件
<button onClick={this.props.addNum}>增加1</button>
但除了上面的用法,mapDispatchToProps 也可以这样用,直接传入一个对象,都没有 dispatch 方法
export default connect(mapStateToProps, { addNumAction })(Demo)
然后只需触发 addNumAction 就能实现和上面一样的效果。
为什么可以不传,当你传入对象的时候, connect 函数会判断,大致代码如下:
let dispatchToProps
if (typeof mapDispatchToProps === 'function') {
dispatchToProps = mapDispatchToProps(store.dispatch)
} else {
// 传递了一个 actionCreator 对象过来
dispatchToProps = bindActionCreators(mapDispatchToProps, store.dispatch)
}
这里就使用了 bindActionCreators 函数,它就是把你传入的 actionCreator 再包一层 dispatch方法,即
{ addNumAction } => (...args) => dispatch(addNumAction(args))
总结
Redux 实现讲到这里就结束了,把原理搞懂了确实对 Redux 的理解加深了好多,之后会继续写相关插件的实现,如 react-redux 等。
参考资料:
- ps: 个人技术博文Github仓库,觉得不错的话欢迎star,给我一点鼓励吧~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。