造一个 redux 轮子
前言吐槽
Redux 应该是很多前端新手的噩梦。还记得我刚接触 Redux 的时候也是刚从 Vue 转过来的时候,觉得Redux 概念非常多,想写一个 Hello World 都难。
文档也是很难看懂,并不是看不懂英文,而是看的时候总会想:TMD在说泥🐴呢。看得出文档想手把手把新手教好,结果却是适得而反,啰嗦的排版和系统性地阐述让新手越来越蒙逼。文档还有一步令人窒息的操作:把 redux、react-redux、redux-toolkit 三个库放在一起来讲。靠,你的标题叫 redux 文档啊,就讲 Redux 不就行了嘛?搞得新手总会觉得 Redux 就是像 Vuex 一样为 React 量身订做的,其实并不是。
Redux 和 React 的关系
Redux 和 React 根本没关系。
看 Redux 的官网开头:"A Predictable State Container for JS Apps"。再看 Vuex 的官网开头:"Vuex is a state management pattern + library for Vue.js applications"。
请问哪里出现了 "react" 这个单词了?
两者的定位本来就不一样:Redux 仅仅是个事件中心(事件总线,随便怎么叫),就是 for JS Apps 的。而 Vuex 除了事件中心,也是 for Vue.js applications 的。
解决了什么问题
为了重新认识 Redux,我们先搞清楚 Redux 到底是个啥、解决了什么问题。
简单来说:
- 创建一个事件中心,里面存一些数据,叫
store
- 向外提供读、写操作,叫
getState
和dispatch
,通过分发事件修改数据,叫dispatch(action)
- 添加监听器,每次 dispatch 数据改了,就触发监听器,达到监听数据变化的效果,叫
subscribe
Redux 本来就是一个超级简单的库,只是文档不知不觉把它写复杂了,搞得新手无从下手,口口相传觉得 Redux 很难、很复杂。其实 Redux 一点都不难、简单得一批。
不信?下面就带大家一起写一个完整的 Redux。
createStore
这个函数创建一个 Object,里面存放数据,并提供读和写方法。实现如下:
function createStore(reduce, preloadedState, enhancer) {
let currentState = preloadedState // 当前数据(状态)
let currentReducer = reducer // 计算新数据(状态)
let isDispatching = false // 是否在 dispatch
// 获取 state
function getState() {
if (isDispatching) {
throw new Error('还在 dispatching 呢,获取不了 state 啊')
}
return currentState
}
// 分发 action 的函数
function dispatch(action) {
if (isDispatching) {
throw new Error('还在 dispatching 呢,dispatch 不了啊')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
return action
}
return {
getState,
dispatch
}
}
上面将数据存于 currentState
。getState
返回当前数据。在 dispatch
里使用 reducer
计算新的数据(状态)从而修改 currentState
。
上面还用 isDispatching
防止多重 dispatch 情况下操作同一资源的问题。
假如别人不给你传 preloadedState
,那 currentState
初始时就会为 undefuned
了呀,undefined
作为 state 是不行的。为了解决这个问题,可以在 createStore
的时候直接 dispatch 一个 action,这个 action 不命中所有 reducer 里的 case,那么 reducer
都返回初始值,以此达到初始化 state 的目的,这也是为什么在 reducer
里的 switch-case 的 default 一定要返回 state 而不是啥都不处理。
// 生成随机字符串,注意这里的 toString(36) 的 36 是基数
const randomString = () => Math.random().toString(36).substring(7).split('').join('.')
const actionTypes = {
INIT: `@@redux/INIT${randomString()}`, // 为了重名,追加随机字符串
}
function createStore(reduce, preloadedState, enhancer) {
...
// 获取 state
function getState() {
...
}
// 分发 action 的函数
function dispatch(action) {
...
}
// 初始化
dispatch({type: actionTypes.INIT})
return {
getState,
dispatch
}
}
然后就可以用我们的 Redux 啦~
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return state + action.payload
case 'decrement':
return state - action.payload
default:
return state
}
}
const store = createStore(reducer, 1) // 1,不管有没有初始值,都会 dispatch @@redux/INIT 来初始化 state
store.dispatch({ type: 'increment', payload: 2 }) // 1 + 2
console.log(store.getState()) // 3
isPlainObject 和 kindOf
Redux 对 action 是有要求的,一定要是普通对象。所以我们还要需要判断一下,如果不是普通对象,就抛出错误并说明 action 此时的类型。
// 分发 action 的函数
function dispatch(action: A) {
if (!isPlainObject(action)) { // 是不是纯对象
throw new Error(`不是纯净的 Object,是一个类似 ${kindOf(action)} 的东西`) // 不是,是一个类似 XXX 的东西
}
...
}
这里的 isPlainObject
和 kindOf
都是可以从 npm 里的 is-plain-object 和 kind-of 获得。这两个包实现都很简单。是不是会觉得:啊?就这?就这么小的包都有几万的下载量???我自己实现也行啊。没错,前端开发就是这么无聊,写这么小的包都能一炮而红,只难当年还不会 JS 没能夺得先机 😢。
这里我们用 npm 包,自己实现一波吧:
首先是 isPlainObject
,一般来说通过判断 typeof obj === 'object'
就可以了,但是 typeof null
也是 object,这是因为最初实现 JS 的时候,用 type 和 value 表示 JS 的值,当 type === 0
时表示是 Object,而当初 null
的地址又为 0x00
所以 null 的 type 一直是 0,因此 typeof null === null
,可以 参考这里。 另一个点是原型键只有一层。
const isPlainObject = (obj: any) => {
// 检查类型
if (typeof obj !== 'object' || obj === null) return false
// 检查是否由 constructor 生成
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
export default isPlainObject
另一个函数 kindOf
实现就繁琐多了,除了要判断一些简单的 typeof 值,还要判断 Array, Date, Error 等多种对象。
const isDate = (value: any) => { // 是不是 Date
if (value instanceof Date) return true
return (
typeof value.toDateString === 'function' &&
typeof value.getDate === 'function' &&
typeof value.setDate === 'function'
)
}
const isError = (value: any) => { // 是不是 Error
if (value instanceof Error) return true
return (
typeof value.message === 'string' &&
value.constructor &&
typeof value.constructor.stackTraceLimit === 'number'
)
}
const getCtorName = (value: any): string | null => { // 获取
return typeof value.constructor === 'function' ? value.constructor.name : null
}
const kindOf = (value: any): string => {
if (value === void 0) return 'undefined'
if (value === null) return 'null'
const type = typeof value
switch (type) { // 有字面意思的值
case 'boolean':
case 'string':
case 'number':
case 'symbol':
case 'function':
return type
}
if (Array.isArray(value)) return 'array' //是不是数组
if (isDate(value)) return 'date' // 是不是 Date
if (isError(value)) return 'error' // 是不是 Error
const ctorName = getCtorName(value)
switch (ctorName) { // 构造函数中读取类型
case 'Symbol':
case 'Promise':
case 'WeakMap':
case 'WeakSet':
case 'Map':
case 'Set':
return ctorName
}
return type
}
上面两个函数在学习 Redux 并不是很重要,不过可以我们提供实现这两个工具函数的一些灵感,下次再次使用时我们也可以直接手写出来。
replaceReducer
replaceReducer
这个函数别说用了,估计没多少人听说过。在 Code Spliting 的时候才会用到。比如打包出来有 2 个 JS,第一个先加载了 reducer,第二个加载新的 reducer,这里可以用 combineReducers
去完成合并。
const newRootReducer = combineReducers({
existingSlice: existingSliceReducer,
newSlice: newSliceReducer
})
store.replaceReducer(newRootReducer)
现在有太多做动态模块、代码分割的库帮我们做了这些事情了,所以我们没多大机会用到这个 API。
实现上也很简单,就是把原来的 reducer
替换掉就可以了。
const actionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`
}
function createStore(reducer, preloadedState, enhancer) {
...
function replaceReducer(nextReducer) {
currentReducer = nextReducer
dispatch({type: actionTypes.REPLACE} as A) // 重新初始化状态
return store
}
...
}
上面除了直接替换,还 dispatch 了 @@redux/REPALCE
这个 action。把当前状态都重置了。
subscribe
刚刚说到 Redux 需要监听数据的变化,非常 Easy ~ 可以在 dispatch 的时候触发所有监听器。
function createStore(reducer, preloadedState, enhancer) {
let currentState = preloadedState
let currentReducer = reducer
let currentListeners = [] // 当前监听器
let nextListeners = currentListeners // 临时监听器集合
let isDispatching = false
// 获取 state
function getState() {
if (isDispatching) {
throw new Error('还在 dispatching 呢,获取不了 state 啊')
}
return currentState
}
// 分发 action 的函数
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(`不是纯净的 Object,是一个类似 ${kindOf(action)} 的东西`)
}
if (isDispatching) {
throw new Error('还在 dispatching 呢,dispatch 不了啊')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
listeners.forEach(listener => listener()) // 全部执行一次
return action
}
// 将 nextListeners 作为临时 listeners 集合
// 防止 dispatching 时出现的一些 bug
function ensureCanMutateNextListeners() {
if (nextListeners !== currentListeners) {
nextListeners = currentListeners.slice()
}
}
// 订阅
function subscribe(listener: () => void) {
if (isDispatching) {
throw new Error('还在 dispatching 呢,subscribe 不了啊')
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener) // 添加监听器
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error('还在 dispatching 呢,unsubscribe 不了啊')
}
isSubscribed = false
ensureCanMutateNextListeners()
// 去掉当前监听器
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
// 初始化
dispatch({type: actionTypes.INIT})
return {
getState,
dispatch,
subscribe,
}
}
上面有几个点要注意:
currentListeners
用于执行监听器,nextListeners
作为临时监听器的存放数组用于增加和移除监听器。弄两个数组是为了防止修改数组数组时出现一些奇奇怪怪的 Bug,和上面用 isDispatching
解决操作同一资源的问题是差不多的。
subscribe
的返回值为 unsubscribe
函数,这一是种很常用的编码设计:如果一个函数有 side-effect,那么返回值最好就是取消 side-effect 的函数,例如 useEffect
里的函数。
可能有人会问如果 subscribe 很多次,第一次的 unsubscribe
里的 listener
还是第一次的 listener 么?这是肯定的,因为 listener
和 unsubscribe
构成了闭包,每次的 unsubscribe
一直会引用那一次的 listener
,listener
不会被销毁。
使用的例子如下:
const store = createStore(reducer, 1)
const listener = () => console.log('hello')
const unsubscirbe = store.subscribe(listener)
// 1 + 2
store.dispatch({ type: 'increment', payload: 2 }) // 打印 "hello"
unsubscribe()
// 3 + 2
store.dispatch({ type: 'increment', payload: 2 }) // 不会打印 "hello"
observable
observable 是 tc39 提出的概念,表示一个可被观察的东西,里面也有一个 subscribe
函数,不同的是传入的参数为 Observer
,这个 Observer
需要有一个 next
函数,将当前状态生成下一个状态。
刚刚已经实现 store 数据的监听了,那 store 也可以看作为一个可被观察的东西。我们弄一个函数就叫 observable
,返回内容即为上面的 observable
的实现:
const $$observable = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')()
export default $$observable
function createStore<S, A extends Action>(reducer preloadedState, enhancer) {
...
// 支持 observable/reactive 库
function observable() {
const outerSubscribe = subscribe
return {
subscribe(observer: unknown) {
function observeState() {
const observerAsObserver = observer
if (observerAsObserver.next) {
observerAsObserver.next(getState())
}
}
observeState() // 获取当前 state
const unsubscribe = outerSubscribe(observeState)
return {unsubscribe}
},
[$$observable]() {
return this
}
}
}
...
}
可以像下面这样去用:
const store = createStore(reducer, 1)
const next = (state) => state + 2 // 获取下一个状态的函数
const observable = store.observable()
observable.subscribe({next}) // 订阅后 next 一下:1 + 2
store.dispatch({type: 'increment', payload: 2}) // 1 + 2 + 3
从上面可以看出,next 的效果就是一个累加的效果。一般人也用不到上面的特性,主要都是别的库会用到,比如 redux-observable 这个轮子。
applyMiddlewares
现在 createStore
已经完成差不多啦,还有第三个参数 enhancer
没有用到。这个函数主要用于增强 createStore
的。在 createStore
里直接传入当前 createStore
,enhance 之后返回一个船新的 createStore
,再传入原来的 reducer
和 preloadedState
生成 store:
function createStore<S, A extends Action>(reducer, preloadedState, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer, preloadedState)
}
...
}
enhancer
函数有很多种实现方式,其中最常见,也是官方提供的就是 applyMiddlewares
这个增强函数。它的目的是通过多种中间件来增强 dispatch
,而 dispatch
又是 store 里的一员,相当于把 store
增强了,因此这个函数是个 enhancer。
在实现 applyMiddlewares
之前,我们要弄清楚中间件这个概念是怎么来的呢?又是如何增强 dispatch
的呢?为啥要用 applyMiddlewares
这个 enhancer 呢?
先从一个简单的例子说起:假如现在我们想在每次 dispatch 后都要 console.log
一下,最简单的方法:直接把 dispatch 改掉:
let originalDispatch = store.dispatch
store.dispatch = (action) => {
let result = originalDispatch(action)
console.log('next state', store.getState())
return result
}
需要注意的是 dispatch 是一个传入 action 并返回 action 的函数,因此这里要将 result 返回出去。
那假如我们再加个 Logger 2 呢?可能会是这样:
const logger1 = (store) => {
let originalDispatch = store.dispatch
store.dispatch = (action) => {
console.log('logger1 before')
let result = originalDispatch(action) // 原来的 dispatch
console.log('logger 1 after')
return result
}
}
const logger2 = (store) => {
let originalDispatch = store.dispatch
store.dispatch = (action) => {
console.log('logger2 before')
let result = originalDispatch(action) // logger 1 的返回函数
console.log('logger2 after')
return result
}
}
logger1(store)
logger2(store)
// logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after
store.dispatch(...)
上面的 logger1 和 logger 2 就叫做中间件,它们可以拿到上一次的 store.dispatch
函数,然后一顿操作生成新的 dispatch
,再赋值到 store.dispatch
来增强 dispatch
。
值得注意的点是,虽然先执行 logger1 再执行 logger2,但是 dispatch 时会以
logger2 before -> logger1 before -> dispatch -> logger1 after -> logger2 after
“倒叙” 的方式来执行中间件的内容。
如果有更多的中间件,可以用数组存起来。初始化也不能像上面那样跑脚本那样初始化了,可以把初始化封装为一个函数,就叫 applyMiddlewares
吧:
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice() // 浅拷贝数组
middlewares.reverse() // 反转数组
// 循环替换dispatch
middlewares.forEach(middleware => store.dispatch = middleware(store))
}
刚刚提到如果正序初始化中间件,会出现“倒序”执行 dispatch 的情况,所以这里要做中间件数组的反转。而 reverse
会改变原数组,因此开头要做一次数组的浅拷贝。
上面的写法有一个问题:在 forEach 里直接改变 store.dispatch 会产生 side-effect。遵循函数式的思路,我们应该生成好一个最终的 dispatch,再赋值到 store.dispatch 上。
怎么生成最终 dispatch 呢?参考 dispatch 的传入 action 返回 action 的思路,我们也可以弄一个传入旧 dispatch 返回新 dispatch 的函数嘛。比如:
const dispatch1 = (dispatch) => {...}
const dispatch2 = (dispatch1) => {...}
const dispatch3 = (dispatch2) => {...}
...
但是这样 store 就传不进来了,不怕,合理运用柯里化可以完美解决我们的问题:
const logger1 => (store) => (next) => (action) => {
console.log('logger1 before')
let result = originalDispatch(action)
console.log('logger 1 after')
return result
}
const logger2 => (store) => (next) => (action) => {
console.log('logger2 before')
let result = originalDispatch(action)
console.log('logger2 after')
return result
}
function applyMiddleware(store, middlewares) {
// 初始的 dispatch
let dispatch = (action) => {
throw new Error('还在构建 middlewares,不要 dispatch')
}
middlewares = middlewares.slice() // 浅拷贝数组
middlewares.reverse() // 反转数组
const middlewareAPI = {
getState: store.getState,
// 这里先用初始的 dispatch,防止在构建过程中 dispatch 的情况
// 如果直接用上面 dispatch 会有闭包的问题,构建的时候都会指向初始时的 dispatch,可能会出现一些奇奇怪怪的 Bug
// 因此这里用了新生成的函数
dispatch: (...args) => dispatch(args)
}
// 怎么生成最终的 dispatch 呢?
const xxx = middlewares.map(middleware => middleware(middlewareAPI))
...
}
为了像上面套娃般地生成新函数,需要用到 reduce
函数来将数组里每个函数进行头接尾尾接头的操作,这样的操作称为 compose
:
function compose(...funcs: Function[]) {
if (funcs.length === 0) {
return (arg) => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((prev, curt) => (...args: any) => prev(curt(...args)))
}
将中间件一个个传入 compose(logger1, logger2)
时,就会出现:
logger1(
logger1 before
logger2(
logger2 before
dispatch -> 最原始的 dispatch
logger2 after
)
logger2 after
)
的结构。这就是 Redux 最厉害的地方了,对中间件的处理十分的优雅,而且使用 reducer
还改变了函数的执行顺序连上面的 reverse
都不需要了。
整理一下上面的改动,再把 applyMiddlewares
写成 enhancer 的写法:
function applyMiddlewares(...middlewares: Middleware[]) {
return (createStore) => (reducer: Reducer, preloadState) => {
const store = createStore(reducer, preloadState)
let dispatch = (action) => {
throw new Error('还在构建 middlewares,不要 dispatch')
}
const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {...store, dispatch}
}
}
到了这一步,你已经掌握了 Redux 的精髓中的精髓了。剩下的就是一些“杂鱼”函数了。
combineReducers
一个非常无聊的函数,仅仅将一堆的 reducer 合并一个 reducer 而已。比如:
const nameReducer = () => '111'
const ageReducer = () => 222
const reducer = combineReducers({
name: nameReducer,
age: ageReducer
})
const store = createStore(reducer, {
name: 'Jack',
age: 18
})
store.dispatch({type: 'xxx'}) // state => {name: '111', age: 222}
怎么合并呢?简单得雅痞:
function combineReducers(reducers: ReducerMapObject) {
return function combination(state, action: AnyAction) {
let hasChanged = false
let nextState = {}
Object.entries(finalReducers).forEach(([key, reducer]) => {
const previousStateForKey = state[key] // 以前的状态
const nextStateForKey = reducer(previousStateForKey, action) // 更新为现在的状态
if (typeof nextStateForKey === 'undefined') {
throw new Error('状态不能是 undefined 啊')
}
nextState[key] = nextStateForKey // 设置最新状态
hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 改了没有啊?
})
// reducer 的 key 的数目和 state 的 key 的数目是否一致
hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length
return hasChanged ? nextState : null
}
}
本质上就是把 reducerMapObject 里每个 reducer 都执行一遍,拿到新 state 更新对应 key 下的 state。当然,Redux 里的对这个函数的实现也没这么简单,它还做了很多异常情况的处理,如检查 reducer 到底是不是合法的 reducer。那啥是合法的 reducer 啊?答:找不到状态时不返回 undefined
就合法。
const randomString = () => Math.random().toString(36).substring(7).split('').join('.')
const actionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
function assertReducerShape(reducers: ReducerMapObject) {
Object.values(reducers).forEach(reducer => {
const initialState = reducer(undefined, {type: actionTypes.INIT})
if (typeof initialState === 'undefined') {
throw new Error('最开始 dispatch 后状态不能为 undefined')
}
const randomState = reducer(undefined, {type: actionTypes.PROBE_UNKNOWN_ACTION})
if (typeof randomState === 'undefined') {
throw new Error('乱 dispatch 后的状态也不能是 undefined')
}
})
}
通过 dispatch @@redux/INIT
和 @@redux/PROBE_UNKNOWN_ACTION
来判断不命中 reducer 里的 case 时有没有返回 undefuned
。当然还检查了 state 啊、action 啊这些东西的合法性:
function getUnexpectedStateShapeWarningMessage(
inputState: object,
reducers: ReducerMapObject,
action: Action,
unexpectedKeyCache: {[key: string]: true}
) {
if (Object.keys(reducers).length === 0) {
return '都没有 reducer 还 combine 个啥呀'
}
if (!isPlainObject(action)) {
return '都说了 action 要是普通的 Object 了,还传一些乱七八糟的东西进来??'
}
if (action.type === actionTypes.REPLACE) return // 因为 replaceReducer,所以这个 reducer 作废了
// 收集 reducerMapObject 里不存在的 key
const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)
unexpectedKeys.forEach(unexpectedKey => unexpectedKeyCache[unexpectedKey] = true)
if (unexpectedKeys.length > 0) {
return `下面这些 Key 都不在 state 上:${unexpectedKeys.join(', ')}`
}
}
这里的 unexpectedKeyCache
是一个 Map,如果某个子 state 有错,则设置为 true
,这个 Map 是为了防止多次告警所做的缓存。
再次更新一下 combineReducers
:
function combineReducers(reducers: ReducerMapObject) {
// 检查是否为函数
let finalReducers: ReducerMapObject = {}
Object.entries(reducers).forEach(([key, reducer]) => {
if (typeof reducer === 'function') {
finalReducers[key] = reducer
}
}, {})
let shapeAssertionError: Error
try {
// 检查 reducer 返回值是否有 undefined
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 用于收集状态不存在的 key
let unexpectedKeyCache: {[key: string]: true} = {}
return function combination(state, action: AnyAction) {
if (shapeAssertionError) throw shapeAssertionError
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
console.log(warningMessage)
}
let hasChanged = false
let nextState = {}
Object.entries(finalReducers).forEach(([key, reducer]) => {
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
throw new Error('状态不能是 undefined 啊')
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
})
// reducer 的 key 的数目和 state 的 key 的数目是否一致
hasChanged = hasChanged || Object.keys(finalReducers).length === Object.keys(state).length
return hasChanged ? nextState : null
}
}
combineActionCreators
更无聊的一个函数:仅仅把多个 action creator 执行,返回一些 () => dispatch(actionCreator(xxx))
的函数,比如:
const store = createStore(reducer, 1)
const combinedCreators = combineActionCreators({
add: (offset: number) => ({type: 'increment', payload: offset}), // 加法 actionCreator
minus: (offset: number) => ({type: 'decrement', payload: offset}), // 减法 actionCreator
}, store.dispatch)
combinedCreators.add(100)
combinedCreators.minus(2)
主要的“好处”是返回的 combinedCreators
里直接 .add(100)
,这里的 .add(100)
可以不用感知 dispatch
的存在。
具体实现如下:
// 绑定一个 actionCreator
function bindActionCreator(actionCreator, dispatch) {
return function (this: any, ...args: any[]) {
return dispatch(actionCreator.apply(this, args))
}
}
// 绑定多个 actionCreator
const combineActionCreators = (actionCreators, dispatch) => {
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
const boundActionCreators: ActionCreatorsMapObject = {}
Object.entries(actionCreators).forEach(([key, actionCreator]) => {
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
})
return boundActionCreators
}
代码非常简单,仅仅帮你执行一下 actionCreator,然后 dispatch 返回的 action。
官方希望的是你在某个地方(比如父组件 combineActionCreators 了),在另外的地方(比如子组件)就不需要拿到 dispatch
函数就可以直接 dispatch action。
理想很好,但是这个功能的前提是要有定义好 actionCreator,一般来说没人会花时间定义 actionCreator,都是直接 dispatch。
总结
上面已经实现整个 redux 里所有的 API 了,基本上是一模一样的,没有偷工减料。
当然,有一些细节,比如判断参数是不是函数,是不是 undefined 是没有做的。为了不写起来太长,比如影响阅读体验,TS 类型也是简单定义,很多函数签名的声明也没有弄。不过这些并不太重要,类型的判断完全可以交给 TS 去做就好了,而 TS 的类型无需太多纠结,毕竟这不是 TS 教程嘛 😆
总结一下,我们都干了什么:
- 实现一个事件总线 + 数据(状态)中心
-
getState
获取数据(状态) -
dispatch(action)
修改数据(状态) -
subscribe(listener)
添加修改数据时的监听器,只要dispatch
所有监听器依次触发 -
replaceReducer
用新 reducer 替换旧 reducer,一般人用不了,忘了吧 -
observable
为了配合 tc39 搞的,准确地说是为了配合 RxJS 搞的。一般人用不起,忘了吧 -
enhancer
传入已有createStore
一通乱搞后返回增强后的createStore
,最最最常见的 enhancer 为applyMiddlewares
。一般人只会用applyMiddlewares
,记住这个就可以了
-
- 实现
applyMiddlewares
,将一堆中间件通过compose
组合起来,执行过程为“洋葱圈”模型。其中中间件的作用是为了增强 dispatch,在 dispatch 前后会做一些事情 - 实现
compose
,原理为将一堆入参为旧 dispatch,返回新 dispatch 的函数数组,使用Array.reduce
组合,变成mid1(mid2(mid3()))
无限套娃的形式 - 实现
combineReducers
,主要作用是将多个 reducer 组件成一个新 reducer,执行 dispatch 后,所有 map 里的 reducer 都会被执行。当你用到了多个子状态Slice
时会用到,别的场景忘了吧 -
combineActionCreators
,将多个 actionCreators 都执行一遍,并返回() => dispatch(actionCreator())
这样的函数。这个直接忘了吧
看到这里,是不是觉得 Redux 其实并没有想象中那么的复杂,所有的“难”,“复杂”只是自己给自己设置的,硬刚源码才能战胜恐惧 👊