前言
时隔 19 个月,再次阅读 redux 源码,发现很多姿势点都有些模糊不清了,但再次阅读很快便又发现其美妙。且这次阅读让我更深入,感受很深。实际核心短短百行代码,运用了 观察者模式,函数式编程的思想,柯里化的运用,属实是美得雅皮~
Redux
正如官网所言,Redux
是 JS 应用 的状态管理器,所以不仅仅局限于 React
,任何 JS 应用 都可以使用它,但由于 Vue
里有官方提供的的 vuex
和 pinia
,所以在 Vue
里实际上用的不多。
React 那里更是状态管理器百花齐放,大家熟知的就有 mobx
, toolkit
, zusand
,所以说现如今可能也比较少人用 redux + react-redux
了,但我们重点是在于学习思路精髓,保持一颗好奇心 forever~
Redux 三大 API
Redux 其重点就是暴露出来的三个 API:createStore(初始化创建store)
, combineReducers(组合Reducer)
, applyMiddleware(改写dispatch)
。(括号里为 API 的主要用途)
这里我只讨论下 createStore
和 applyMiddleware
,且会按照自己阅读源码后写的代码来说明,差别也不大,思路都是一致的。小伙伴可以对照着来~
1. createStore
/**
* 创建一个 store
*
* @param {*} reducer 一个纯函数,接收 state 和 action 作为参数,返回一个新的state
* @param {*} initState 初始状态
* @param {*} middleware 中间件
* @returns 返回一个包含了 获取状态方法, 监听方法, 派发方法 的对象
*/
const createStore = (reducer, initState, middleware) => {
let currentState = initState // 赋值初始值
let listenerMap = new Map()
let listenerCount = 1 // listenerMap 的 key值
if (typeof middleware == 'function') {
// 如果有中间件,就去处理。这里源码用了 函数柯里化 的写法
// 当然如果你自己写,也可以用 middleware(createStore, reducer, initState) 的方式
// 这里不过多讨论,但柯里化还是挺帅的
// 后面会讨论中间件的处理,这里暂且略过
return middleware(createStore)(reducer, initState)
}
// 获取状态的方法,相当简单
const getState = () => {
return currentState
}
// 该方法就是将 监听函数 存放到 listenerMap 中,等到后面调用 dispatch 时,循环调用 监听函数。
// 这里我做了个小改动,多加了一个 options ,如果存在 lazy 属性且为true,则首次监听不调用listener()
const subscribe = (listener, options) => {
let listenerId = listenerCount++
listenerMap.set(listenerId, listener)
// lazy 为 true 则首次进入不调用监听方法
!options?.lazy && listener?.(currentState)
// 返回一个解除监听的方法
return function unsubscribe() {
listenerMap.delete(listenerId)
}
}
// 派发方法
const dispatch = (action) => {
// 调用 reducer ,去更新 state
const state = reducer(currentState, action)
if (state != currentState) {
currentState = state
// 遍历并调用监听方法
listenerMap.forEach((listener) => {
listener?.(currentState)
})
}
}
return {
getState,
subscribe,
dispatch,
}
}
这段代码缩减下来才几十行,源码里有对类型进行处理,这里就不细说了。简单概括下, createStore
就是运用了观察者模式,当用户调用 dispatch
时,去更新 state 并且调用监听方法
来,用上面的方法来尝试下~
const myReducer = (state, action) => {
switch (action.type) {
case 'CHANGE_NAME':
return { ...state, name: action.payload }
default:
return state
}
}
// 初始化store
const store = createStore(myReducer, { name: '王尼玛' })
// 监听函数
const unsubscribe = store.subscribe((state) => {
alert(`我是${state.name}`)
})
// 一秒后调用 dispatch 将 name 修改为 牛尼玛
setTimeout(() => {
store.dispatch({
type: 'CHANGE_NAME',
payload: '牛尼玛',
})
}, 1000)
2. 中间件
有些时候,我们想让 dispatch
不单纯只是去修改 state
,而是能做一些其他的操作,这时候我们就要用到中间件去处理了。
可能有朋友问,为什么不直接 reducer
里做,首先我们要明确, reducer
是一个纯函数,这是约定熟成的,所以我们不应该去对 reducer
进行处理,而是应该在 dispatch
这做处理。
我们用一个简单的情况来说明,假设想让每次 dispatch
时,都能打印一个日志:显示出调用了什么类型的 reducer。这时候要如何做?
很显然我们既然是处理 dispatch
,那么直接
const myDispatch = (action) => {
store.dispatch(action) // 调用原 dispatch
console.log('日志:', action.type) // 打印日志
}
myDispatch({
type: 'CHANGE_NAME',
payload: '牛尼玛',
})
这就是中间件的作用,但是实际运用情况并不是如此简单,我可能有多种功能,但如果把各种功能全部放到 myDispatch
里处理,则显得很冗余。
比如我现在想在打印一个日志 2,那要如何做到不把代码写在一堆?
源码里引入了函数式编程的思想。
// 中间件1
const logDispatch = (next) => {
return (action) => {
let result = next(action)
console.log('日志:', action.type, action.payload)
}
}
// 中间件2
const log2Dispatch = (next) => {
return (action) => {
let result = next(action)
console.log('日志2:,测试中间件')
}
}
// 注意顺序,想要最先调用的需要在最里面。
const myDispatch = log2Dispatch(logDispatch(store.dispatch))
// 调用 myDispatch 后,打印...
// 日志: CHANGE_NAME 牛尼玛
// 日志2:,测试中间件
我们可以将每个中间件拆解成各个模块,每个模块内部返回一个 函数。
这里会有点绕,我们一步一步拆解。
// 最里层
logDispatch(store.dispatch)
看声明我们知道 store.dispatch
就是 next
这里返回的函数,接收一个 action
,是不是和 dispatch
很像,也是接收一个 action
而 next
又是 dispatch
,所以如果最里层被调用了,就是调用了 dispatch(action)
但他还没被调用,先不急。
// 上一层
log2Dispatch(logDispatch(store.dispatch))
log
和 log2
其实都一样,接收的参数,返回的值都一样,这也是中间件的硬性要求,总不能让用户随便写随便用,所以 中间件函数 都有相同参数,返回值的规范。
但是注意这里,log2
的 next
变成了 log(store.dispatch)
,且 log2
也是返回一个接收 action
的函数。那么当我们调用 myDispatch
并且传入一个 action
进去时。
先进入 log2
方法,然后 log2
会调用 next(action)
,相当于调用了 log(store.dispatch)(action)
再进入到 log
,他的 next
为 store.dispatch
,那么就相当于调用
store.dispatch(action)
我们目的已经达到了,紧接着执行完后返回到 log
里,继续执行 log 的中间件操作,执行完成, 返回上一个函数,返回到了 log2
,执行 log2 的中间件操作。
ok,到了这一步,我们已经基本上已经完成了中间件的操作,但是一般中间件都是引入的第三方插件,我们不应该让用户手动去调用 log2Dispatch(logDispatch(store.dispatch))
这点 redux
考虑到了~
3. applyMiddleware
前面我们在 createStore
处有提到过,如果有中间件的情况时,会去这样处理
if (typeof middleware == 'function') {
return middleware(createStore)(reducer, initState)
}
// 作为用户,我们只需要将各个中间件传给 applyMiddleware ,让他去处理即可
const store = createStore(myReducer, { name: '王尼玛' }, applyMiddleware(logDispatch, log2Dispatch))
// 由于是内部直接改写 dispatch 的,我们也不需要去取别名,直接调用 store.dispatch 即可触发中间件操作
是不是就显得清晰了很多。我们来瞅瞅 applyMiddleware
的源码
/**
* 将用户传入的中间件,处理成 f1(f2(dispatch)) 的格式
*
* @param {...any} wares 各个中间件
* @returns
*/
const applyMiddleware = (...wares) => {
return (createStore) =>
(...args) => {
// 老样子,创建一个 store
const store = createStore(...args)
// 这个dispatch可以忽略,后面都是用改写过的dispatch了
let dispatch = () => {
throw new Error('你不对劲')
}
// 需要注意的是,中间件有强制的格式要求
// 源码里的 中间件格式要求 和我上面写的不一样,他还多套了一层 store 的函数
/**
* const logDispatch = (store) => {
* return (next) => {
* return (action) => {
* let result = next(action)
* console.log('日志:', action.type, action.payload)
* }
* }
* }
*/
const waresAPI = {
getState: store.getState(),
dispatch: (action, ...args) => dispatch(action, ...args),
}
// 但在这里你会发现,经过这个 map 的处理,chins 就和我上面写的中间件的格式一摸一样了
const chins = wares.map((ware) => ware(waresAPI))
// 这里可以直接赋值 store.dispatch ,但这样区分开,便于理解
// 千万不要把 originDispatch 和 (dispatch 或 next) 混淆起来。
// 他们是不同的东西,最基础的是 originDispatch
const originDispatch = store.dispatch
// 重点 compose 方法
dispatch = compose(...chins)(originDispatch)
// 返回改写好的 dispatch
return {
...store,
dispatch,
}
}
}
我们主要分析下 compose
方法,可以看到它也是用了函数柯里化
const compose = (...funcs) => {
// 源码里用的是 reduce ,所以 redux 的中间件执行顺序是从右往左
// 我这里用了 reduceRight ,所以使得中间件执行顺序变成了从左往右
return funcs.reduceRight((f1, f2) => {
return (...args) => {
return f1(f2(...args))
}
})
}
compose(...chins)
中, chins
是一个 函数数组
// chins 中的元素都为如下形式
const f1 = (next) => {
return (action) => {
let result = next(action)
// ...
}
}
我们以前面用到的 log
和 log2
两个中间件来举例,在加入一个 log3
的中间件便于理解
// chins 就相当于 log, log2.... 可以传入无限个中间件
compose(log, log2, log3)
// 根据数组的 reduceRight 方法,他会对元素执行我提供的 (...args) => { return f1(f2(...args))} ,且上一次的输出会作为下一次的输入
return [log, log2, log3].reduceRight((f1, f2) => {
return (...args) => {
return f1(f2(...args))
}
})
// 由于我的是 reduceRight ,所以 f1 为 log3, f2 为 log2
// 这里就变成了 log2(log(...args))
// 然后由于 reduce 的方法:上一次的输出会作为下一次的输入
// 所以接下来 f1 变成了
let f1 = (...args) => {
return log3(log2(...args))
}
// f2 变成了 log
let f2 = (log = (next) => {
return (action) => {
let result = next(action)
console.log('日志:', action.type, action.payload)
}
})
// 将 log 代入
let last = (...args) => {
return f1(log(...args))
}
// 在将 f1 转换成
last = (...args) => {
return log3(log2(log(...args)))
}
// 最后在执行
last(originDispatch)
// 就得到了
log3(log2(log(originDispatch)))
累加器这块如果还有疑问,可以多瞅瞅其他地方的讲解,我可能讲的不是很好(doge)
最后这个 log3(log2(log(originDispatch)))
就是经过我们中间件改写过后的 dispatch
了。
compose
这种结合函数的方式,不仅在 redux
这里有,我也在 elementUI
的源码里有瞄到过,但是没有仔细去看,非常不错的思路,希望大家可以记住
留言
这次重读 Redux
源码,深有感触,代码量虽然不多,但设计巧妙,易读,非常适合初接触源码的小伙伴研究~