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 其实并没有想象中那么的复杂,所有的“难”,“复杂”只是自己给自己设置的,硬刚源码才能战胜恐惧 👊