Redux系列-深入理解Redux

257

〇、前言

学习RN开发已有一段时间,这段时间最大的感受是,RN页面开发最麻烦的就是状态管理。RN基于前端框架React来构建页面,React一开始给类组件提供了setState方法用于更新组件状态,这种更新状态的方式有着自己的局限性,当一个页面上有多个组件,涉及多种状态的时候,setState方式会使得状态变得难以管理和预测。后来React又推出了可以使用Hooks的函数式组件,本质上是对State进行粒度上的拆分,使得函数式组件也可以保存状态,但是依然没解决状态管理麻烦的问题,为了解决这一问题,也涌现出了一些状态管理库,其中比较著名的就是Redux了,虽然Redux宣称自己是不是为React专门打造,可以适用于各种JS项目,不过应该很少有人将Redux用在除React的别的框架中,包括Redux官方文档的示例都是基于React~

关于Redux的使用介绍可以参考这篇文章,这里更多的是做原理的探讨。

一、深入理解store、action、reducer

1.1 store

首先思考一个问题:store是什么,以及为什么需要store?

1.1.1 store是什么

store的英文意思是‘存储’,顾名思义,就是用来存储状态的地方。

1.1.1 为什么需要store

我们已经知道 store 是存储状态的地方,那么什么是状态呢?状态就是用来填充UI的数据。状态可以是一行文本,可以是一个布尔值,可以是一个数字...也可以是由这些基本类型构成的数组或对象,我们通过这些状态数据来填充当前的UI样式。

为什么需要store呢?自然是因为我们需要一个地方来保存当前页面的状态,这个保存页面状态的地方就是store,当然store不是必须要保存当前页面所有组件的所有状态,一些不需要其他组件了解的状态,可以在组件内部,自己使用setState或useState进行状态管理。

1.1.2 createStore做了什么

在我们常规的使用中,createStore方法传入一个reducer,返回一个store,为了深入了解 createStore,我们可以看一下这个方法的源码

接下来对源码进行一些删减,主要是删减一些边界条件的判断和报错,但是依然可以保证基本功能的使用。

import ActionTypes from './utils/actionTypes'

// 传入三个参数,分别是reducer(必填),初始的默认state(可选),store增强(可选)
export default function createStore(reducer, preloadedState, enhancer) {
	
  let currentReducer = reducer // reducer 赋值 currentReducer
  let currentState = preloadedState // currentState 是真正保存状态的地方
  let currentListeners = [] // 通过 store.subscribe 注册的回调都保存在此列表中
  let nextListeners = currentListeners // 监听器副本,防止在dispatch事件的时候调用 注册或解注册方法而发生冲突

  /**
   * 确保nextListeners持有的是 currentListeners 的副本
   */
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 常用方法,getState,本质上就是返回了当前state
  function getState() {
    return currentState
  }
	
  // 常用方法,subscribe, 注册一个监听器
  function subscribe(listener) {
    let isSubscribed = true // 已注册的标志位

    ensureCanMutateNextListeners() // 确保nextListeners持有和currentListeners内容一致的副本
    nextListeners.push(listener) // 将监听添加到nextListeners 中

    // 返回 unsubscribe 方法,用于解注册
    return function unsubscribe() { 
      if (!isSubscribed) { // 如果已经解注册了就直接返回
        return
      }
      isSubscribed = false // 设置标志位
      ensureCanMutateNextListeners() // 确保nextListeners持有和currentListeners内容一致的副本
      const index = nextListeners.indexOf(listener) 
      nextListeners.splice(index, 1) // 从nextListener中删除该监听注册
      currentListeners = null // 当前注册列表置空,在下次dispatch的时候,会重新将 currentListeners 赋值为 nextListeners
    }
  }

  // 常用方法 dispatch,用于发送action
  function dispatch(action) {
    currentState = currentReducer(currentState, action) // 调用reducer,传入当前state和要发送的action,得到新state

    const listeners = (currentListeners = nextListeners) // 将currentListeners指向nextListeners并赋值给listeners变量
    // 遍历调用所有注册的监听
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
		// 返回发送的action
    return action
  }

  // 替换reducer,不太常用
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }
  // 发送一个 INIT事件,发送这个事件的目的在于给currentState赋值,让其使用reducer里的默认state
  dispatch({ type: ActionTypes.INIT })

  // 最终返回一个state对象,主要包含这四个方法
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer
  }
}

上面的代码是可以直接在项目中使用来替代 redux 库中的 createStore方法的,没什么问题,注释已经写的很详细,接下来对两个不太好理解的地方进行解释:

  • 为什么要有 currentListeners 和 nextListeners 两个list?答:为了防止在dispatch的时候,注册了新的监听或解注册监听导致冲突,比如我们正在循环遍历注册列表进行回调,这时候突然注册了个新回调,或者解注册了个回调,列表发生了变化,这就不太对了,我们希望在dispatch函数调用的过程中,所遍历的列表是不可变的,所以我们对注册列表的增加和删除操作,都变成了在 nextListeners 这个副本上进行,不会影响正在遍历的列表。
  • 最终为什么要发送 dispatch({ type: ActionTypes.INIT }) ,答:为了初始化state。在dispatch方法中会调用reducer,如果用户自定义的reducer没有对 ActionTypes.INIT 做处理,会返回 reducer设置的默认状态,如果做了处理,就返回用户自定义的默认状态。

1.2 action

1.2.1 action是什么

action的英文意思是动作,动作的目的是为了改变当前状态。

1.2.2 action结构

action的结构非常简单,可以是这样的

{
	type: 'refresh'
}

type 用于标识动作类型,比如上面的这个action就可能代表的是一个刷新事件。

需要注意的是,action只是一个普普通通的对象,Redux规范中要求使用 type 字段代表动作类型,那么你不用 type 字段,而是用一个 leixing 字段代表action类型可以不,

嗯,可以是可以,毕竟 reducer 也是你自己写的,只需要在 reducer 中对 leixing 字段作匹配并返回新state也不是不行,但是像之前在createStore源码中看到的 { type: ActionTypes.INIT } 事件就收不到了,不建议这么做。

Redux的action规范除了规定type这个用于标注动作类型的必须字段外,还有几个可选字段

  • payload: 用于携带数据,可以是基本数据类型,也可以是对象
  • error: 错误信息字段
  • meta: action的额外描述字段

更加详细的规范可以参考这个

1.3 reducer

reducer,reducer?

reducer为什么叫reducer?这是一个有意思的问题,毕竟 store 和 action 的命名很好理解,那么这个reducer是...减速器?

1.3.1 reducer名字的由来

关于reducer名字的由来,就要提到 JS Array对象的 reduce方法了

var numbers = [65, 44, 12, 4];
 
function getSum(total, num) {
    return total + num;
}

numbers.reduce(getSum);
// 输出结果:125

上面代码块中,我们定义了一个数组 numbers,调用了数组的 reduce 方法,该方法中传入 getSum 方法作为参数,reduce方法会使用getSum方法依次遍历数组中元素,这个getSum 函数就被称为 reducer 函数,

reducer函数getSum接收两个参数,其中total是之前遍历的数组元素的累加,num是当前遍历到的数组元素,我们将两个加起来后return 出去,这个返回值就会成为遍历下个元素时 getSum 方法的total参数,我们这样就计算出了数组元素的累加和。

因为Redux中的 reducer 和 数组 reduce 方法使用的 reducer 有异曲同工之妙,所以 Redux 的reducer就叫 reducer 了。

有一说一,感觉 reducer 这个名字起的不太好,一些不太了解JS的人看到 reducer这个名字就完全不知道是用来干嘛的,个人认为,或许起名叫 StateCalculator 更合适一些~

1.3.2 reducer的作用

reducer的作用是根据当前state和要发送的action计算并返回一个新的state。

1.3.3 reducer的结构

const initialState = {
	refresh: false
}

export default function appReducer(state = initialState, action) {
  // 根据action的type字段匹配acton要做的事情
  switch (action.type) {
    // 在这里定义不同action类型所要做的事情,并返回新状态
    case 'refresh': {
    	return {
      	...state,
        refresh: true
      }
    }
    default:
      // 如果没匹配到对应type,返回原state
      return state
  }
}

reducer接收当前state和发送的action作为参数,返回一个修改后的新的state。

在 createStore 方法中我们知道,reducer方法会在 store的 dispatch 方法中被调用,并用其返回值修改 store的state,接下来会遍历所有通过store.subscribe方法注册的监听,监听方法中会使用最新的状态更新UI。

总结

现在我们知道了

  • store充当一个保存状态的存储器,可以通过 getState方法获取当前状态;通过dispatch方法发送动作,并更新存储的状态,然后遍历回调所有监听;通过subscribe方法注册回调,该回调在store每次dispatch action的时候被调用。
  • action是一个普通的对象,用来传递动作类型和数据
  • reducer 是一个接收当前state和发送的action作为参数,返回值类型为state的普通函数,充当一个状态计算器,根据当前状态和store dispatch的动作类型来算出新的状态。

一句话概括:

redux的本质就是监听store中的数据变更,在store中的数据更改的时候对通过subscribe注册的监听者进行回调。特别的是数据更改的方式是通过action和reducer来计算。