造一个 redux 轮子

文章源码:https://github.com/Haixiang6123/my-redux

参考轮子:https://www.npmjs.com/package/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
  • 向外提供读、写操作,叫 getStatedispatch,通过分发事件修改数据,叫 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
  }
}

上面将数据存于 currentStategetState 返回当前数据。在 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 的东西
  }
  ...
}

这里的 isPlainObjectkindOf 都是可以从 npm 里的 is-plain-objectkind-of 获得。这两个包实现都很简单。是不是会觉得:啊?就这?就这么小的包都有几万的下载量???我自己实现也行啊。没错,前端开发就是这么无聊,写这么小的包都能一炮而红,只难当年还不会 JS 没能夺得先机 😢。

这里我们用 npm 包,自己实现一波吧:

首先是 isPlainObject,一般来说通过判断 typeof obj === 'object' 就可以了,但是 typeof null 也是 object,这是因为最初实现 JS 的时候,用 typevalue 表示 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 么?这是肯定的,因为 listenerunsubscribe 构成了闭包,每次的 unsubscribe 一直会引用那一次的 listenerlistener 不会被销毁。

使用的例子如下:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容