「React」一文带你了解 Redux

忘是亡心i 2022-12-31 11:22 89阅读 0赞

目录

        1. Redux 核心概念
        1. Redux 数据管理
        1. Redux 适用场景
        1. Redux 代码组织方式
        1. Redux API
        • (1)createStore
        • (2)Store
        • (3)State
        • (4)Action
        • (5)Action Creator
        • (6)store.dispatch()
        • (7)Reducer
        • (8)store.subscribe()
        1. Redux 最佳实践
        1. Flux 架构思想
        1. Redux 源码解读
        • (1)目录结构
        • (2)createStore源码

在这里插入图片描述

1. Redux 核心概念

先来看一下官方对 Redux 的描述:

Redux 是 JavaScript 状态容器,它提供可预测的状态管理。

来看一下这句话背后的深意:

  • Redux 是为JavaScript应用而生的,也就是说它不是 React 的专利,React 可以用,Vue 可以用,原生 JavaScript 也可以用;
  • Redux 是一个状态容器,什么是状态容器?来看个例子:

假如把一个 React 项目里面的所有组件拉进一个群,那么 Redux 就充当了这个群里的“群文件”角色,所有的组件都可以把需要在组件树里流动的数据存储在群文件里。当某个数据改变的时候,其他组件都能够通过下载最新的群文件来获取到数据的最新值。这就是“状态容器”的含义——存放公共数据的仓库

应用的状态往往十分复杂,如果应用状态就是一个普通 JavaScript 对象,而任何能够访问到这个对象的代码都可以修改这个状态,就很容易乱了套。当 bug 发生的时候,我们发现是状态错了,但是也很难理清到底谁把状态改错了,到底是如何走到出 bug 这一步。Redux 的主要贡献,就是限制了对状态的修改方式,让所有改变都可以被追踪。

Redux 基本使用:
(1)在使用Redux之前,我们需要安装稳定版的Redux包。
因为 Redux 是一个中立的状态管理工具,和 React 没有直接联系。所以,如果在 React 应用中使用 Redux,除了要引入 Redux,还需要导入 react-redux 这个安装包,安装方法如下:

  1. npm install --save redux react-redux

(2)在使用Redux时,需要将其引入组件:

  1. import { createStore } from 'redux';

3. Redux 数据管理

Redux 主要由三部分组成:store、reducer 和 action。先来看看它们各自代表什么:

  • store:就好比组件群里的“群文件”,它是一个单一的数据源,而且是只读的;
  • action:就是“动作”的意思,它是对变化的描述。

举个例子,下面这个对象就是一个 action:

  1. const action = {
  2. type: "ADD_ITEM",
  3. payload: '<li>text</li>'
  4. }
  • reducer 是一个函数,它负责对变化进行分发和处理,最终将新的数据返回给 store。

store、action 和 reducer 三者紧密配合,便形成了 Redux 独树一帜的工作流:
在这里插入图片描述

从上图可以看出,在 Redux 的整个工作过程中,数据流是严格单向的。

对于一个 React 应用来说,视图(View)层面的所有数据(state)都来自 store(再一次诠释了单一数据源的原则)。如果想对数据进行修改,只有一种途径:派发 action。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。

对于组件来说,任何组件都可以通过约定的方式从 store 读取到全局的状态,任何组件也都可以通过合理地派发 action 来修改全局的状态。Redux 通过提供一个统一的状态容器,使得数据能够自由而有序地在任意组件之间穿梭, 这就是 Redux 实现组件间通信的思路。

3. Redux 适用场景

当一个 React 应用采用 Redux 之后,对于某个状态,到底是放在 Redux 的 Store 中呢,还是放在 React 组件自身的状态中呢?

  • 如果所有状态全都放在 Redux 的 Store 上,那就要对应增加 reducer 和 action 的代码,虽然拥有了可以跟踪的好处,但是对一些很细小的状态也要增加 reducer 和 action,会感觉得不偿失。
  • 如果状态放在 React 组件中,感觉又白白浪费了 Redux 的优势,回到了 React 原生管理状态的老路上去。

面对这种左右为难的纠结状况,我们可以通过以下三步来确定:
(1)第一步,看这个状态是否会被多个 React 组件共享。
所谓共享,就是多个组件需要读取或者修改这个状态,如果是,那不用多想,应该放在 Store 上,因为 Store 上状态方便被多个组件共用,避免组件之间传递数据;如果不是,继续看第二步。
(2)第二步,看这个组件被 unmount 之后重新被 mount,之前的状态是否需要保留。
举个例子,一个对话框组件。用户在对话框打开的时候输入了一些内容,不做提交直接关闭这个对话框,这时候对话框就被 unmount 了,然后重新打开这个对话框(也就是重新 mount),需求是否要求刚才输入的内容依然显示?如果是,那么应该把状态放在 Store 上,因为 React 组件在 unmount 之后其中的状态也随之消失了,要想在重新 mount 时重获之前的状态,只能把状态放在组件之外,Store 当然是一个好的选择;如果需求不要求重新 mount 时保持 unmount 之前的状态,继续看第三步。
(3)第三步,到这一步,基本上可以确定,这个状态可以放在 React 组件中了。
不过,如果你觉得这个状态很复杂,需要跟踪修改过程,那看你个人喜好,可以选择放在 Store 上;如果你想简单处理,可以心安理得地让这个状态由 React 组件自己管理。

当然,对于简单状态,尽量还是用 React 自己来搞定,只有那些适用场合不限于一个组件的,才有足够理由让 Redux 来管理。

4. Redux 代码组织方式

在应用中引入 Redux 之后,就会引入 action 和 reducer。从方便管理的角度出发,和 React 组件一样,action 和 reducer 都有自己独立的源代码文件,很自然,我们需要决定如何组织这些代码。

更好的方法,是把源代码文件分类放在不同的目录中,根据分类方式,可以分为两种:

  • 基于角色的分类(role based)
  • 基于功能的分类(feature based)

(1)基于角色的分类
在MVC 应用中,在一个目录下放所有的 controller,在另一个目录下放所有的 view,在第三个目录下放所有的 model,每个目录下的文件都是同样的“角色”,这就是基于角色的分类。对应到使用 React 和 Redux 的应用,做法就是把所有 reducer 放在一个目录(通常就叫做 reducers),把所有 action 放在另一个目录(通常叫 actions)。最后,把所有的纯 React 组件放在另一个目录。

(2)基于功能的分类
基于功能的分类方式,是把一个模块相关的所有源代码放在一个目录。例如,对于博客系统,有 Post(博客文章)和 Comment(注释)两个基本模块,建立两个目录 Post 和 Comment,每个目录下都有各自的 action.jsreducer.js 文件,如下所示,每个目录都代表一个模块功能,这就是基于功能的分类方式。

  1. Post -- action.js
  2. |_ reucer.js
  3. |_ view.js
  4. Comment -- action.js
  5. |_ reucer.js
  6. |_ view.js

一般说来,基于功能的分类方式更优。因为每个目录是一个功能的封装,方便共享。具体用哪种方式来组织代码,主要就看是否预期这些模块会被共享,如果会,那采用基于功能的方式就是首选。

5. Redux API

(1)createStore

  1. const store = createStore(
  2. reducer,
  3. initial_state,
  4. applyMiddleware(middleware1, middleware2, ...)
  5. );

createStore 方法是一切的开始,它接收三个入参:

  • reducer;
  • 初始状态内容;
  • 指定中间件;

一般来说,只有 reducer 是必须有的。

(2)Store

Store 就是保存所有状态数据的地方,整个应用只能有一个 Store。使用 createStore 函数来创建 Store:

  1. import { createStore } from 'redux'
  2. const reducer = (state, action) => {
  3. // ...
  4. return new_state
  5. }
  6. const store = createStore(reducer)

使用 createStore 函数接收了一个 reducer 函数,这里 createStore 接收了一个函数,并返回了 store。

(3)State

Store 对象包含了所有数据,如果想获取某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State。

对于 state,有三大原则:

  1. 唯一数据源:所有的状态值保存在 Redux 的 store 中,整个应用只有一个 store,状态是一个树形对象,每个组件使用状态树上的一部分数据;
  2. 保持状态只读:在任何时候都不能直接修改应用状态。只能通过发送一个 Action,由这个 Action 描述如何去修改应用状态;
  3. 只有纯函数能改变数据:这里的函数指的就是 reducer,它接收两个参数,第一个参数是 state,也就是当前状态,第二个参数是 actionreducer 根据这两个参数的值,创建并返回一个新的对象。

    import { createStore } from ‘redux’
    const reducer = (state, action) => {

    1. // ...
    2. return new_state

    }
    const store = createStore(reducer)
    // 获取state
    const state = store.getState()

注意:这里说的纯函数是指不依赖于且不改变它作用域之外的变量的函数,也就是说函数返回的结果必须完全由传入的参数决定

(4)Action

State 的变化,会导致 View 的变化。用户只能接触到 View 视图层,所以,我们 State 的变化必然是由于 View 导致的。视图层 View 通过 Action 发出通知,告知 State 它该上场了,需要发生改变了。

那么 Action 是什么呢?它是一个对象,type 属性是其必须的,它标识 Action 的名称,可能会有的属性有三个:errorpayloadmeta

  1. const action = {
  2. type: 'ADD_TODO',
  3. payload: 'Learn Redux'
  4. }

上面代码中,给 Action 定义了一个名称: “ADD_TODO”,它携带的是字符串 “Learn Redux”。

注意: Action 是改变 State 的唯一方式。

(5)Action Creator

View 会发送多种消息,这就需要定义多种 Action,如果每个都手写,会重复工作。可以定义一个函数来生成 Action,这个函数就称为 Action Creator。

  1. const ADD_TODO = 'ADD_TODO'
  2. function addTodo(type, text) {
  3. return {
  4. type,
  5. payload: text
  6. }
  7. }
  8. const action = addTodo(ADD_TODO, 'Learn Redux')

函数 addTodo 就是一个 Action Creator。

(6)store.dispatch()

Action 是改变 state 的唯一方式,Action 是 View 发出的,那么 View 是怎样发出 Action 呢?这时就需要用到 store.dispatch()

store.dispatch() 接收一个 Action 对象作为参数,并将它发送出去。

  1. import { crateStore } from 'redux'
  2. const reducer = (state, action) => {
  3. // ...
  4. return new_state
  5. }
  6. const store = createStore(reducer)
  7. store.dispatch({
  8. type: 'ADD_TODO',
  9. payload: 'Learn Redux'
  10. })

注意:store.dispatch() 是 View 发出 Action 的唯一方法。

(7)Reducer

Store 收到 Action 之后,会返回一个新的 State,此时 View 会更新。State 的计算过程就称为 Reducer。Reducer 是一个函数,接收两个参数 Action 和当前的 State,并返回一个新的 State:

  1. const reducer = (state, action) => {
  2. // ...
  3. return new_state
  4. }

Reducer 不需要手动调用,store.dispatch() 方法会触发 Reducer 的自动执行,就是在生成 Store 的时候,将 Reducer 传入 createStore 方法,上面的 Store、State 就一直在使用。

(8)store.subscribe()

Store 允许使用 store.subscribe() 方法设置监听函数,State 发生变化时会自动执行这个函数。

  1. import { createStore } from 'redux'
  2. const reducer = (state, action) => {
  3. // ...
  4. return new_state
  5. }
  6. const store = createStore(reducer)
  7. store.subscribe(listener)

store.subscribe 方法返回一个函数,调用这个函数就可以解除监听。

  1. let unsubscribe = store.subscribe(() =>
  2. console.log(store.getState())
  3. )
  4. unsubscribe()

通过上面的API,总结出Redux的工作流如下:
在这里插入图片描述

6. Redux 最佳实践

应用 Redux 的时候,有这些业界已经证明的最佳实践:
(1)Store 上的数据应该范式化。
所谓范式化,就是尽量减少冗余信息,像设计 MySQL 这样的关系型数据库一样设计数据结构。
(2)使用 selector
对于 React 组件,需要的是『反范式化』的数据,当从 Store 上读取数据得到的是范式化的数据时,需要通过计算来得到反范式化的数据。你可能会因此担心出现问题,这种担心不是没有道理,毕竟,如果每次渲染都要重复计算,这种浪费积少成多可能真会产生性能影响,所以,我们需要使用 seletor。业界应用最广的 selector 就是 reslector 。

reselector 的好处,是把反范式化分为两个步骤,第一个步骤是简单映射,第二个步骤是真正的重量级运算,如果第一个步骤发现产生的结果和上一次调用一样,那么第二个步骤也不用计算了,可以直接复用缓存的上次计算结果。
绝大部分实际场景中,总是只有少部分数据会频繁发生变化,所以 reselector 可以避免大量重复计算。
(3)只 connect 关键点的 React 组件
当 Store 上状态发生改变的时候,所有 connect 上这个 Store 的 React 组件会被通知:状态改变了!
然后,这些组件会进行计算。connect 的实现方式包含 shouldComponentUpdate 的实现,可以阻挡住大部分不必要的重新渲染,但是,毕竟处理通知也需要消耗 CPU,所以,尽量让关键的 React 组件 connect 到 store 就行。

一个实际的例子就是,一个列表种可能包含几百个项,让每一个项都去 connect 到 Store 上不是一个明智的设计,最好是只让列表去 connect,然后把数据通过 props 传递给各个项。

7. Flux 架构思想

Redux 的设计在很大程度上受益于 Flux 架构,可以说 Redux 是 Flux 的一种实现形式。Redux 结合了 Flux 架构和函数式编程,Flux 是一种架构思想,专门解决软件的结构问题。它跟 MVC 架构是同一类东西,但是更加简单和清晰。

Flux 并不是一个具体的框架,它是一套由 Facebook 技术团队提出的应用架构,这套架构约束的是应用处理数据的模式。在 Flux 架构中,一个应用将被拆分为以下 4 个部分:

  • View(视图层):用户界面。该用户界面可以是以任何形式实现出来的,React 组件是一种形式,Vue、Angular 也完全 OK。Flux 架构与 React 之间并不存在耦合关系。
  • Action(动作):也可以理解为视图层发出的“消息”,它会触发应用状态的改变。
  • Dispatcher(派发器):它负责对 action 进行分发。
  • Store(数据层):它是存储应用状态的“仓库”,此外还会定义修改状态的逻辑。store 的变化最终会映射到 view 层上去。

这 4 个部分之间的协作将通过下图所示的工作流规则来完成配合:
在这里插入图片描述

从上图可以看出,Flux 的最大特点,就是数据的”单向流动”,保证了整个执行流程的清晰。

Flux 在运行中,会按如下过程执行:

  • 用户访问 View 视图层;
  • 触发了用户的 Action;
  • Dispatcher 收到 Action 的动作,通知 Store 需要更新数据;
  • Store 更新数据后,发出 “change” 事件通知,通知 View 数据发生了变化,需要更新 UI;
  • View 在接收到 “change” 事件通知后,会去更新页面。

Flux 最核心的地方在于严格的单向数据流,在单向数据流下,状态的变化是可预测的。如果 store 中的数据发生了变化,那么有且仅有一个原因,那就是由 Dispatcher 派发 Action 来触发的。这样一来,就从根本上避免了混乱的数据关系,使整个流程变得清晰简单。

不过这并不意味着 Flux 是完美的。事实上,Flux 对数据流的约束背后是不可忽视的成本:除了开发者的学习成本会提升外,Flux 架构还意味着项目中代码量的增加。

Flux 架构往往在复杂的项目中才会体现出它的优势和必要性。如果项目中的数据关系并不复杂,其实完全轮不到 Flux 登场,这一点对于 Redux 来说也是一样的。

结合 Flux 架构的特性,来看 Redux 官方给出的这个定义,就更容易理解可预测的深意了:

Redux 是 JavaScript 状态容器,它提供可预测的状态管理。

8. Redux 源码解读

(1)目录结构

下面是Redux源码的目录结构:
在这里插入图片描述

其中,utils 是工具方法库;index.js 作为入口文件,用于对功能模块进行收敛和导出。真正工作的是功能模块本身,也就是下面这几个文件:

  • applyMiddleware.js: 中间件模块
  • bindActionCreators.js: 用于将传入的 actionCreator 与 dispatch 方法相结合,合并一个新的方法
  • combineReducers.js: 用于将多个 reducer 合并起来
  • compose.js: 用于把接收到的函数从右向左进行组合
  • createStore.js: 整个流程的入口,也是 Redux 中最核心的 API

(2)createStore源码

下面主要来看下createStore.js的源码(目前github上的源码是TS版的,这里以JS版为例)。

使用 Redux 的第一步,就是调用 createStore 方法。这个方法做的事情似乎就是创建一个 store 对象出来,像这样:

  1. // 引入 redux
  2. import { createStore } from 'redux'
  3. // 创建 store
  4. const store = createStore(
  5. reducer,
  6. initial_state,
  7. applyMiddleware(middleware1, middleware2, ...)
  8. );

从拿到入参到返回出 store 的过程中,到底都发生了什么呢?我们来看一下 createStore 中主体逻辑的源码:

  1. function createStore(reducer, preloadedState, enhancer) {
  2. // 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况
  3. if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
  4. // 此时第二个参数会被认为是 enhancer(中间件)
  5. enhancer = preloadedState;
  6. preloadedState = undefined;
  7. }
  8. // 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中
  9. if (typeof enhancer !== 'undefined') {
  10. return enhancer(createStore)(reducer, preloadedState);
  11. }
  12. // 记录当前的 reducer,因为 replaceReducer 会修改 reducer 的内容
  13. let currentReducer = reducer;
  14. // 记录当前的 state
  15. let currentState = preloadedState;
  16. // 声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件
  17. let currentListeners = [];
  18. // nextListeners 是 currentListeners 的快照
  19. let nextListeners = currentListeners;
  20. // 该变量用于记录当前是否正在进行 dispatch
  21. let isDispatching = false
  22. // 该方法用于确认快照是 currentListeners 的副本,而不是 currentListeners 本身
  23. function ensureCanMutateNextListeners() {
  24. if (nextListeners === currentListeners) {
  25. nextListeners = currentListeners.slice();
  26. }
  27. }
  28. // 我们通过调用 getState 来获取当前的状态
  29. function getState() {
  30. return currentState;
  31. }
  32. // subscribe 订阅方法,它将会定义 dispatch 最后执行的 listeners 数组的内容
  33. function subscribe(listener) {
  34. // 校验 listener 的类型
  35. if (typeof listener !== 'function') {
  36. throw new Error('Expected the listener to be a function.')
  37. }
  38. // 禁止在 reducer 中调用 subscribe
  39. if (isDispatching) {
  40. throw new Error(
  41. 'You may not call store.subscribe() while the reducer is executing. ' +
  42. 'If you would like to be notified after the store has been updated, subscribe from a ' +
  43. 'component and invoke store.getState() in the callback to access the latest state. ' +
  44. 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
  45. )
  46. }
  47. // 该变量用于防止调用多次 unsubscribe 函数
  48. let isSubscribed = true;
  49. // 确保 nextListeners 与 currentListeners 不指向同一个引用
  50. ensureCanMutateNextListeners();
  51. // 注册监听函数
  52. nextListeners.push(listener);
  53. // 返回取消订阅当前 listener 的方法
  54. return function unsubscribe() {
  55. if (!isSubscribed) {
  56. return;
  57. }
  58. isSubscribed = false;
  59. ensureCanMutateNextListeners();
  60. const index = nextListeners.indexOf(listener);
  61. // 将当前的 listener 从 nextListeners 数组中删除
  62. nextListeners.splice(index, 1);
  63. };
  64. }
  65. // 定义 dispatch 方法,用于派发 action
  66. function dispatch(action) {
  67. // 校验 action 的数据格式是否合法
  68. if (!isPlainObject(action)) {
  69. throw new Error(
  70. 'Actions must be plain objects. ' +
  71. 'Use custom middleware for async actions.'
  72. )
  73. }
  74. // 约束 action 中必须有 type 属性作为 action 的唯一标识
  75. if (typeof action.type === 'undefined') {
  76. throw new Error(
  77. 'Actions may not have an undefined "type" property. ' +
  78. 'Have you misspelled a constant?'
  79. )
  80. }
  81. // 若当前已经位于 dispatch 的流程中,则不允许再度发起 dispatch(禁止套娃)
  82. if (isDispatching) {
  83. throw new Error('Reducers may not dispatch actions.')
  84. }
  85. try {
  86. // 执行 reducer 前,先"上锁",标记当前已经存在 dispatch 执行流程
  87. isDispatching = true
  88. // 调用 reducer,计算新的 state
  89. currentState = currentReducer(currentState, action)
  90. } finally {
  91. // 执行结束后,把"锁"打开,允许再次进行 dispatch
  92. isDispatching = false
  93. }
  94. // 触发订阅
  95. const listeners = (currentListeners = nextListeners);
  96. for (let i = 0; i < listeners.length; i++) {
  97. const listener = listeners[i];
  98. listener();
  99. }
  100. return action;
  101. }
  102. // replaceReducer 可以更改当前的 reducer
  103. function replaceReducer(nextReducer) {
  104. currentReducer = nextReducer;
  105. dispatch({ type: ActionTypes.REPLACE });
  106. return store;
  107. }
  108. // 初始化 state,当派发一个 type 为 ActionTypes.INIT 的 action,每个 reducer 都会返回
  109. // 它的初始值
  110. dispatch({ type: ActionTypes.INIT });
  111. // observable 方法可以忽略,它在 redux 内部使用,开发者一般不会直接接触
  112. function observable() {
  113. // observable 方法的实现
  114. }
  115. // 将定义的方法包裹在 store 对象里返回
  116. return {
  117. dispatch,
  118. subscribe,
  119. getState,
  120. replaceReducer,
  121. [$$observable]: observable
  122. }
  123. }

通过上面源码会发现,createStore 从外面看只是一个简单的创建动作,但在内部却涵盖了所有 Redux 主流程中核心方法的定义。

createStore 内部逻辑如下:
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,89人围观)

还没有评论,来说两句吧...

相关阅读