redux为什么这么难用

2,851

本来想写一篇redux吐槽的,整篇都在阴阳怪气,但是后来发现没什么意思,怎么写也超不过500字,就修改重写了,改成了redux入门和redux的原理实现

1 redux核心思想

redux中,有3个核心思想需要了解

  • Store:存储数据的地方(仓库)
  • Reducer:改变数据的地方
  • Action:告诉Reducer如何改变数据

2 redux入门

2.1 Action

首先了解一下ActionAction就是一个对象,类型定义如下。在Reducer中通过Actiontype来判断要如何更新数据

interface Action<T = any> {
  type: T
}

但是大多数情况下,一个type是不够的(增删改),还需要给action携带一个数据,扩展如下

interface MyAction<T = any, D = any> extends Action<T> {
  data: D
}

2.2 Reducer

Reducer类型定义如下,Reducer就是一个方法,此方法第一个参数就是存储在Store中的数据,第二个参数就是Action,返回的S就是修改后的数据

type Reducer<S = any, A extends Action> = (
  state: S | undefined,
  action: A
) => S

Reducer工作流程如下

  • 第一步:开发者想要更新Store,创建一个对象,这个对象在redux中就是Action
  • 第二步:调用redux提供的方法,把Action传进去
  • 第三步:redux内部调用ReducerReducer内部根据传入的Action,修改第一个参数,并把修改后的值return出来
  • 第四步:redux内部接收到Reducer的返回值,更新数据

之后为了解耦,Reducer可以拆分成无限个函数方便管理,但是这意味着,每次更新的时候,redux内部需要把所有的Reducer全部执行一遍用于获取新的状态,有没有感觉这和React的思想非常相似

2.3 Store

上面两个说的都是概念,需要配合Store一起使用才有意义

2.3.1 createStore

那么就需要引出redux的核心方法createStore。此方法就是用来创建“数据”管理的工具,先看下面这个简化后的类型定义

interface Dispatch<A extends Action> {
  <T extends A>(action: T): T
}

interface Store<S = any, A extends Action = Action> {
  // 获取当前存储在Store中的数据
  getState(): S
  // 修改数据
  dispatch: Dispatch<A>
  // 用于订阅数据变化
  subscribe(listener: () => void): () => void
  // 用于替换reducer,这里不实现
  // replaceReducer
}

type createStore = <S, A extends Action>(
  // reducer
  reducer: Reducer<S, A>
) => Store<S, A>

接着就可以根据上面的类型定义写一个Store

import { createStore } from 'redux'
import type { Action } from 'redux'

function reducer(count = 0, action: Action<'increase' | 'decrease'>) {
  switch (action.type) {
    case 'increase': return ++count
    case 'decrease': return --count
    default: return count
  }
}

const store = createStore(reducer)

“非常简单”的一个仓库就创建好了,接下来要干的事情无非就下面三个

  • 获取仓库中存储的数据
  • 修改仓库中存储的数据
  • 监听仓库中存储的数据的变化

正好这三个需要对应了createStore的三个返回值 getStatedispatchsubscribe

2.3.2 getState

getState是用来获取仓库中存储的数据的方法

store.getState()  // 0

2.3.3 dispatch

此方法用来修改仓库中存储的数据,它的参数就是Action

store.getState()  // 0
store.dispatch({ type: 'increase' })
store.getState()  // 1
store.dispatch({ type: 'increase' })
store.getState()  // 2

2.3.4 subscribe

此方法用来订阅数据变化

store.subscribe(()=>{
  console.log('updated')    // 调用两次
})

store.dispatch({ type: 'increase' })
store.dispatch({ type: 'increase' })

并且subscribe还会返回一个函数,用于取消订阅

const unsubscribe = store.subscribe(()=>{})

unsubscribe()  // 取消监听

2.4 combineReducers

如果整个项目中只能存在一个Reducer,那这个Reducer一定非常大,可以对它进行拆分,拆分完成之后,再通过 redux 提供的 combineReducers方法进行整合

function reducer1(count = 0, action: Action<'increase' | 'decrease'>) {
  switch (action.type) {
    case 'increase': return ++count
    case 'decrease': return --count
    default: return count
  }
}

function reducer2(count = 1, action: Action<'increase' | 'decrease'>) {
  switch (action.type) {
    case 'increase': return ++count
    case 'decrease': return --count
    default: return count
  }
}

const store = createStore(combineReducers({
  count1: reducer1,
  count2: reducer2
}))

store.getState()         // {count1: 0, count2: 1}

当我们调用dispatch想要修改count1的时候,就会发现下面的问题,count2也一起更改了

console.log(store.getState())         // {count1: 0, count2: 1}
store.dispatch({ type: 'increase' })
console.log(store.getState())         // {count1: 1, count2: 2}

这是因为redux内部会执行所有的Reducerincrease这个type两个Reducer中都使用到了,所以就会引发问题,为了解决这个问题,有两种方式解决

  • 使用symbol来当做key
  • 约定好type,不能重复

2.5 bindActionCreators

每次修改数据的时候,都必须做两件事情

  • 寻找要修改数据的那个type
  • 调用dispatch

很麻烦,所以redux提供了bindActionCreators方法,能够把上面两个步骤合并成一件事情

import { createStore, bindActionCreators } from 'redux'

function reducer(count = 0, action: Action<'increase' | 'decrease'>) {
  switch (action.type) {
    case 'increase': return ++count
    case 'decrease': return --count
    default: return count
  }
}

const store = createStore(reducer)

const actions = bindActionCreators(
  {
    increase: () => ({ type: 'increase' }),
    decrease: () => ({ type: 'decrease' })
  },
  store.dispatch
)

actions.increase()
actions.increase()

2.6 中间件

redux还提供了中间件,在redux中的中间件就是一个方法,只是这个方法有点特殊,如下所示,为了方便理解,在定义方法的时候,使用a,b,c为子函数命名

code.png

那么a函数中的参数api的类型定义如下,其中包含两个函数,dispatchgetState

interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> {
  dispatch: D
  getState(): S
}

你没有猜错,getStatecreateStore返回的getState表现性质一致,但是dispatch却不同,在redux源码中,此处的dispatch如果调用它的话,只会得到一个警告错误,所以请永远不要在中间件中调用api.dispatch

code-16452502482121.png

那么b函数中的参数next是什么?其实这个next就是"dispatch方法",只不过此dispatch并不是上面理解的dispatch方法。在redux中,需要使用 applyMiddleware 组合所有的中间件,如下

const store = createStore(
  reducer,
  applyMiddleware(
    middleware1,
    middleware2
  )
)

因此,想要知道第二个参数的next是啥,就得去了解一下 applyMiddlewarecreateStore的关系,如下图所示

image-20220219142615543.png

从上图可以看到,创建Store的这个操作其实是在applyMiddleware中进行的,有点套娃的意思,那么现在需要搞懂的自然就是调用createStoreend之前发生了什么?

第一步,applyMiddleware内部定义了一个dispatch函数,并与createStore返回的getState方法进行了一个组装,这个组装好的对象就是传递给中间件的参数

let dispatch: Dispatch = () => {
  throw new Error(
    'Dispatching while constructing your middleware is not allowed. ' +
    'Other middleware would not be applied to this dispatch.'
  )
}

const middlewareAPI: MiddlewareAPI = {
  getState: store.getState,
  dispatch
}

第二步,调用所有的中间件,并获取中间件的返回值(b函数)

const chain = middlewares.map(middleware => middleware(middlewareAPI))

第三步,applyMiddleware把所有的chain都传入到了 compose 方法中

dispatch = compose(chain)(store.dispatch)

先不看 compose(chain)后面的(store.dispatch),先看 compose是做什么的,核心代码如下(为了好理解做了部分修改)

function compose(funcs: Function[]) {
  return funcs.reduce((a, b) => {
    return (...args: any) => {
      return a(b(...args))
    }
  })
}

为了理解,看下面这个例子。比如此时,funcs的值是

[ b1, b2, b3, b4, b5, b6 ]

分别对应middleware1middleware2...的返回值,那么在reduce的循环中,第一次调用回调的形参a和b分别是b1b2,那么返回值可以看成

(...args) => {
	return b1(b2(...args))
}

第二次循环中,形参a为上一次循环的返回值,bb3,那么返回值可以看成

(...args) => {
	return b1(b2(b3(...args)))
}

以此类推,最后compose就会返回一个这样的函数

(...args) => {
	return b1(b2(b3(b4(b5(b6(...args))))))
}

接着,把store.dispatch传入到此函数中,那么效果如下

middleware6的b函数的next形参就是store.dispatch

middleware5的b函数的next形参就是middleware6的b函数的返回值c函数

middleware4的b函数的next形参就是middleware5的b函数的返回值c函数

middleware3的b函数的next形参就是middleware4的b函数的返回值c函数

middleware2的b函数的next形参就是middleware3的b函数的返回值c函数

middleware1的b函数的next形参就是middleware2的b函数的返回值c函数

重新生成的dispatch就是middleware1的b函数的返回值c函数

那么最后一步,把store和新的dispatch函数组装,返回

return {
	...store,
	dispatch
}

那么到此为止,中间件中第三个参数action其实就很明确了,就是在创建好Store后,调用dispatch传递的参数,如果所有的中间件都在c函数中调用下面这句话

next(action)

那么就会形成一个链式调用,所有的action都会“流过”中间件,最后到达redux调用reducers,并触发更新订阅

image-20220219151424815.png

这也意味着,只要有一个中间件中止流动,后面的所有行为都不会触发

image-20220219151749370.png

3 写一个redux

接下来就自己写一个redux,当然这个redux是个”残废版“,只实现核心逻辑,具体的参数验证都不做了,具体代码托管到了git

3.1 createStore

如果你看过redux的源码,其中大部分的代码都是在验证数据,那么createStore的核心逻辑只有获取数据,更新数据,订阅数据变化的逻辑,40行就可以实现

function createStore(reducer) {

  // 记录当前的state
  let currentState
  // 记录订阅的所有函数
  let currentListeners = new Set()

  const getState = () => currentState

  const subscribe = (listener) => {

    // 添加到散列表中
    currentListeners.add(listener)

    // 避免重复移除订阅
    let isSubscribed = true

    // 取消订阅
    return function unsubscribe() {
      if (!isSubscribed) return
      isSubscribed = false
      currentListeners.delete(listener)
    }

  }

  const dispatch = (action) => {

    // 获取新state
    currentState = reducer(currentState, action)

    // 触发订阅
    for (let listener of currentListeners) {
      listener()
    }

  }

  // 初始化调用一次reducer,获取初始数据
  dispatch({ type: null })

  return { getState, subscribe, dispatch }

}

3.2 combineReducers

combineReducers 用于组装reducers,那么只需要做一个遍历执行所有reducers就能获取下一次状态了,在redux中,做了一个浅比较,如果state相对于上次没有发生变化的话,还返回上次的state

function combineReducers(reducers) {

  return (state = {}, action) => {

    // 准备一个标识,判断数据是否发生变化
    let hasChanged = false
    // 存储下一次状态
    const nextState = {}

    // 遍历执行所有reducer
    for (let key in reducers) {
      const reducer = reducers[key]
      const previousStateForKey = state[key]
      nextState[key] = reducer(previousStateForKey, action)
      
      if(!hasChanged && nextState[key] !== previousStateForKey){
        hasChanged = true
      }

    }

    // 返回下一次状态
    return hasChanged ? nextState : state

  }

}

3.3 bindActionReducers

bindActionReducers就更简单了,通过遍历就可以实现

function bindActionCreators(actions, dispatch) {

  const cb = {}

  for(let key in actions){
    cb[key] = function () {
      dispatch(actions[key](...arguments))
    }
  }

  return cb
}

3.4 applyMiddleware

需要先在createStore中把创建Store的主动权交给appleMiddleware

function createStore(reducer, enhancer) {

  // 交给中间件创建Store
  if (enhancer) return enhancer(createStore, reducer)
 	
  ...
  
}

接着applyMiddleware按照中间件那节讲述的逻辑实现

function applyMiddleware(...middlewares) {
  return function (createStore, reducer) {

    const store = createStore(reducer)

    const middlewareAPI = {
      dispatch: () => { throw new Error('不要在初始化的时候调用dispatch') },
      getState: store.getState
    }

    const chain = middlewares.map(f => f(middlewareAPI))
    store.dispatch = compose(chain)(store.dispatch)

    return store

  }
}

function compose(funcs) {

  if (!funcs.length) return f => f

  if (funcs.length === 1) return funcs[0]

  return funcs.reduce((a, b) => {
    return function () {
      return a(b(...arguments))
    }
  })

}

4 redux的问题

如果你一直使用redux作为状态管理工具,那么想要增加一个状态的时候,必然会执行下面的步骤

  • 定义一个不重复的type
  • 定义action类型
  • 编写Reducer

这其实是非常不直观的,直观的方法是直接定义一个变量,直接修改它就好了

let data = 1
data = 2

但这是做不到响应式,如果能做的到的话,尤雨溪就不要用各种骚操作去解决ref.value的问题了,所以只能无限的逼近这个体验,我们现在来看看同为数据管理的mobx是怎么解决这个问题的

import { makeAutoObservable, action, observable, autorun } from 'mobx'

class Store {

  count = 0
  
  constructor(){
    makeAutoObservable(this)
  }

  increase() {
    this.count++
  }

}

const store = new Store()

autorun(() => {
  console.log('updated')
})

store.increase()					
													// 控制台打印:updated
console.log(store.count)  // 控制台打印:1

如果你没有学过 mobx,看上面的代码,也能瞬间上手,不需要去管内部的实现逻辑,对象怎么用,它就怎么用

第一节就说过redux的核心思想就是存数据,使用action对象标记怎么更新数据,再由Reducer来进行更改数据,描述成流程图如下

image-20220217103945589.png

而在mobx中,Action是用来修改数据的方法, 就绕过了reduder,借助Proxy的能力,开发者可以直接通过Action修改Store上的数据

image-20220217105823077.png

redux中修改数据这个操作全部由Reducer完成,而Reducer恰恰是心智负担最重的地方,这也是我觉得redux难用的点