JS状态管理器 Redux 源码

207 阅读8分钟

前言

时隔 19 个月,再次阅读 redux 源码,发现很多姿势点都有些模糊不清了,但再次阅读很快便又发现其美妙。且这次阅读让我更深入,感受很深。实际核心短短百行代码,运用了 观察者模式,函数式编程的思想,柯里化的运用,属实是美得雅皮~

Redux

正如官网所言,ReduxJS 应用状态管理器,所以不仅仅局限于 React ,任何 JS 应用 都可以使用它,但由于 Vue 里有官方提供的的 vuexpinia ,所以在 Vue 里实际上用的不多。

React 那里更是状态管理器百花齐放,大家熟知的就有 mobx, toolkit, zusand ,所以说现如今可能也比较少人用 redux + react-redux 了,但我们重点是在于学习思路精髓,保持一颗好奇心 forever~

Redux 三大 API

Redux 其重点就是暴露出来的三个 API:createStore(初始化创建store), combineReducers(组合Reducer), applyMiddleware(改写dispatch)。(括号里为 API 的主要用途)

这里我只讨论下 createStoreapplyMiddleware,且会按照自己阅读源码后写的代码来说明,差别也不大,思路都是一致的。小伙伴可以对照着来~

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))

loglog2 其实都一样,接收的参数,返回的值都一样,这也是中间件的硬性要求,总不能让用户随便写随便用,所以 中间件函数 都有相同参数,返回值的规范。

但是注意这里,log2next 变成了 log(store.dispatch),且 log2 也是返回一个接收 action 的函数。那么当我们调用 myDispatch 并且传入一个 action 进去时。

先进入 log2 方法,然后 log2 会调用 next(action) ,相当于调用了 log(store.dispatch)(action)

再进入到 log ,他的 nextstore.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)
    // ...
  }
}

我们以前面用到的 loglog2 两个中间件来举例,在加入一个 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 源码,深有感触,代码量虽然不多,但设计巧妙,易读,非常适合初接触源码的小伙伴研究~