「React」一文带你了解 Redux 忘是亡心i 2022-12-31 11:22 61阅读 0赞 ### 目录 ### * * * 1. Redux 核心概念 * 3. Redux 数据管理 * 3. Redux 适用场景 * 4. Redux 代码组织方式 * 5. Redux API * * (1)createStore * (2)Store * (3)State * (4)Action * (5)Action Creator * (6)store.dispatch() * (7)Reducer * (8)store.subscribe() * 6. Redux 最佳实践 * 7. Flux 架构思想 * 8. Redux 源码解读 * * (1)目录结构 * (2)createStore源码 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyMDMzNTY3_size_16_color_FFFFFF_t_70_pic_center] ### 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 这个安装包,安装方法如下: npm install --save redux react-redux (2)在使用Redux时,需要将其引入组件: import { createStore } from 'redux'; ### 3. Redux 数据管理 ### Redux 主要由三部分组成:store、reducer 和 action。先来看看它们各自代表什么: * store:就好比组件群里的“群文件”,它是一个单一的数据源,而且是**只读**的; * action:就是“动作”的意思,它是对变化的描述。 举个例子,下面这个对象就是一个 action: const action = { type: "ADD_ITEM", payload: '<li>text</li>' } * reducer 是一个函数,它负责对变化进行分发和处理,最终将新的数据返回给 store。 store、action 和 reducer 三者紧密配合,便形成了 Redux 独树一帜的工作流: ![在这里插入图片描述][20201229154038346.jpg] 从上图可以看出,**在 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.js` 和 `reducer.js` 文件,如下所示,每个目录都代表一个模块功能,这就是基于功能的分类方式。 Post -- action.js |_ reucer.js |_ view.js Comment -- action.js |_ reucer.js |_ view.js 一般说来,基于功能的分类方式更优。因为每个目录是一个功能的封装,方便共享。具体用哪种方式来组织代码,主要就看是否预期这些模块会被共享,如果会,那采用基于功能的方式就是首选。 ### 5. Redux API ### #### (1)createStore #### const store = createStore( reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) ); createStore 方法是一切的开始,它接收三个入参: * reducer; * 初始状态内容; * 指定中间件; 一般来说,只有 reducer 是必须有的。 #### (2)Store #### Store 就是保存所有状态数据的地方,整个应用只能有一个 Store。使用 `createStore` 函数来创建 Store: import { createStore } from 'redux' const reducer = (state, action) => { // ... return new_state } const store = createStore(reducer) 使用 `createStore` 函数接收了一个 reducer 函数,这里 `createStore` 接收了一个函数,并返回了 store。 #### (3)State #### Store 对象包含了所有数据,如果想获取某个时点的数据,就要对 Store 生成快照,这种时点的数据集合,就叫做 State。 对于 state,有三大原则: 1. **唯一数据源**:所有的状态值保存在 Redux 的 `store` 中,整个应用只有一个 `store`,状态是一个树形对象,每个组件使用状态树上的一部分数据; 2. **保持状态只读**:在任何时候都不能直接修改应用状态。只能通过发送一个 `Action`,由这个 `Action` 描述如何去修改应用状态; 3. **只有纯函数能改变数据**:这里的函数指的就是 `reducer`,它接收两个参数,第一个参数是 `state`,也就是当前状态,第二个参数是 `action`。`reducer` 根据这两个参数的值,创建并返回一个新的对象。 import { createStore } from 'redux' const reducer = (state, action) => { // ... 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 的名称,可能会有的属性有三个:`error`、`payload` 和 `meta`: const action = { type: 'ADD_TODO', payload: 'Learn Redux' } 上面代码中,给 Action 定义了一个名称: “ADD\_TODO”,它携带的是字符串 “Learn Redux”。 **注意:** Action 是改变 State 的唯一方式。 #### (5)Action Creator #### View 会发送多种消息,这就需要定义多种 Action,如果每个都手写,会重复工作。可以定义一个函数来生成 Action,这个函数就称为 Action Creator。 const ADD_TODO = 'ADD_TODO' function addTodo(type, text) { return { type, payload: text } } const action = addTodo(ADD_TODO, 'Learn Redux') 函数 `addTodo` 就是一个 Action Creator。 #### (6)store.dispatch() #### Action 是改变 state 的唯一方式,Action 是 View 发出的,那么 View 是怎样发出 Action 呢?这时就需要用到 `store.dispatch()`。 `store.dispatch()` 接收一个 Action 对象作为参数,并将它发送出去。 import { crateStore } from 'redux' const reducer = (state, action) => { // ... return new_state } const store = createStore(reducer) store.dispatch({ type: 'ADD_TODO', payload: 'Learn Redux' }) **注意:**`store.dispatch()` 是 View 发出 Action 的唯一方法。 #### (7)Reducer #### Store 收到 Action 之后,会返回一个新的 State,此时 View 会更新。State 的计算过程就称为 Reducer。Reducer 是一个函数,接收两个参数 Action 和当前的 State,并返回一个新的 State: const reducer = (state, action) => { // ... return new_state } Reducer 不需要手动调用,`store.dispatch()` 方法会触发 Reducer 的自动执行,就是在生成 Store 的时候,将 Reducer 传入 `createStore` 方法,上面的 Store、State 就一直在使用。 #### (8)store.subscribe() #### Store 允许使用 `store.subscribe()` 方法设置监听函数,State 发生变化时会自动执行这个函数。 import { createStore } from 'redux' const reducer = (state, action) => { // ... return new_state } const store = createStore(reducer) store.subscribe(listener) `store.subscribe` 方法返回一个函数,调用这个函数就可以解除监听。 let unsubscribe = store.subscribe(() => console.log(store.getState()) ) unsubscribe() 通过上面的API,总结出Redux的工作流如下: ![在这里插入图片描述][20201229154003527.jpg] ### 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 个部分之间的协作将通过下图所示的工作流规则来完成配合: ![在这里插入图片描述][20201229153934597.png] 从上图可以看出,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源码的目录结构: ![在这里插入图片描述][20201229153914838.png] 其中,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 对象出来,像这样: // 引入 redux import { createStore } from 'redux' // 创建 store const store = createStore( reducer, initial_state, applyMiddleware(middleware1, middleware2, ...) ); 从拿到入参到返回出 store 的过程中,到底都发生了什么呢?我们来看一下 createStore 中主体逻辑的源码: function createStore(reducer, preloadedState, enhancer) { // 这里处理的是没有设定初始状态的情况,也就是第一个参数和第二个参数都传 function 的情况 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { // 此时第二个参数会被认为是 enhancer(中间件) enhancer = preloadedState; preloadedState = undefined; } // 当 enhancer 不为空时,便会将原来的 createStore 作为参数传入到 enhancer 中 if (typeof enhancer !== 'undefined') { return enhancer(createStore)(reducer, preloadedState); } // 记录当前的 reducer,因为 replaceReducer 会修改 reducer 的内容 let currentReducer = reducer; // 记录当前的 state let currentState = preloadedState; // 声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件 let currentListeners = []; // nextListeners 是 currentListeners 的快照 let nextListeners = currentListeners; // 该变量用于记录当前是否正在进行 dispatch let isDispatching = false // 该方法用于确认快照是 currentListeners 的副本,而不是 currentListeners 本身 function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice(); } } // 我们通过调用 getState 来获取当前的状态 function getState() { return currentState; } // subscribe 订阅方法,它将会定义 dispatch 最后执行的 listeners 数组的内容 function subscribe(listener) { // 校验 listener 的类型 if (typeof listener !== 'function') { throw new Error('Expected the listener to be a function.') } // 禁止在 reducer 中调用 subscribe if (isDispatching) { throw new Error( 'You may not call store.subscribe() while the reducer is executing. ' + 'If you would like to be notified after the store has been updated, subscribe from a ' + 'component and invoke store.getState() in the callback to access the latest state. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' ) } // 该变量用于防止调用多次 unsubscribe 函数 let isSubscribed = true; // 确保 nextListeners 与 currentListeners 不指向同一个引用 ensureCanMutateNextListeners(); // 注册监听函数 nextListeners.push(listener); // 返回取消订阅当前 listener 的方法 return function unsubscribe() { if (!isSubscribed) { return; } isSubscribed = false; ensureCanMutateNextListeners(); const index = nextListeners.indexOf(listener); // 将当前的 listener 从 nextListeners 数组中删除 nextListeners.splice(index, 1); }; } // 定义 dispatch 方法,用于派发 action function dispatch(action) { // 校验 action 的数据格式是否合法 if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } // 约束 action 中必须有 type 属性作为 action 的唯一标识 if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } // 若当前已经位于 dispatch 的流程中,则不允许再度发起 dispatch(禁止套娃) if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { // 执行 reducer 前,先"上锁",标记当前已经存在 dispatch 执行流程 isDispatching = true // 调用 reducer,计算新的 state currentState = currentReducer(currentState, action) } finally { // 执行结束后,把"锁"打开,允许再次进行 dispatch isDispatching = false } // 触发订阅 const listeners = (currentListeners = nextListeners); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } return action; } // replaceReducer 可以更改当前的 reducer function replaceReducer(nextReducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.REPLACE }); return store; } // 初始化 state,当派发一个 type 为 ActionTypes.INIT 的 action,每个 reducer 都会返回 // 它的初始值 dispatch({ type: ActionTypes.INIT }); // observable 方法可以忽略,它在 redux 内部使用,开发者一般不会直接接触 function observable() { // observable 方法的实现 } // 将定义的方法包裹在 store 对象里返回 return { dispatch, subscribe, getState, replaceReducer, [$$observable]: observable } } 通过上面源码会发现,createStore 从外面看只是一个简单的创建动作,但在内部却涵盖了所有 Redux 主流程中核心方法的定义。 createStore 内部逻辑如下: ![在这里插入图片描述][20201229153840950.jpg] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyMDMzNTY3_size_16_color_FFFFFF_t_70_pic_center]: /images/20221120/072f830f994f49699d9b90738ae11e60.png [20201229154038346.jpg]: /images/20221120/05d0b5d9bd0148cba6d2389c7304853d.png? [20201229154003527.jpg]: https://img-blog.csdnimg.cn/20201229154003527.jpg? [reslector]: https://github.com/reduxjs/reselect [20201229153934597.png]: https://img-blog.csdnimg.cn/20201229153934597.png? [20201229153914838.png]: https://img-blog.csdnimg.cn/20201229153914838.png? [20201229153840950.jpg]: https://img-blog.csdnimg.cn/20201229153840950.jpg?
相关 一文带你了解WeakHashMap 1. WeakHashMap的适用场景 大部分的缓存都需要占用内存,考虑到内存的有限性,并不能缓存所有的对象,此时就需要用到另类的集合,例如WeakHashMap,可以保 àì夳堔傛蜴生んèń/ 2023年02月21日 06:38/ 0 赞/ 76 阅读
相关 「React」一文带你了解 Redux 目录 1. React 组件状态 (1)什么数据放在 state 中 (2)修改 state 冷不防/ 2023年01月10日 13:28/ 0 赞/ 379 阅读
相关 「React」一文带你了解React diff 算法 目录 1. diff 算法 (1)tree diff (2)component diff Dear 丶/ 2023年01月01日 06:48/ 0 赞/ 224 阅读
相关 「React」一文带你了解虚拟 DOM 目录 1. 虚拟 DOM 的概念 2. 虚拟 DOM 的工作流程 3. 虚拟DOM可以带来更好的性能吗? 我就是我/ 2023年01月01日 05:47/ 0 赞/ 213 阅读
相关 「React」一文带你了解 Redux 目录 1. Redux 核心概念 3. Redux 数据管理 3. Redux 适用场景 忘是亡心i/ 2022年12月31日 11:22/ 0 赞/ 62 阅读
相关 「React」一文带你了解 React-Router 目录 1. React Router概述 2. 环境准备 3. 路由器 (1)B 梦里梦外;/ 2022年12月31日 03:25/ 0 赞/ 202 阅读
相关 一文带你了解React生命周期 目录 1. React生命周期概览 2. 组件挂载阶段 (1)constructor 淡淡的烟草味﹌/ 2022年10月29日 09:26/ 0 赞/ 348 阅读
相关 一文带你了解React 纯函数组件 1. 纯函数 所谓纯函数,它是这样一种函数:即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。 从纯函数的定义,可以提取出纯函数的必要条件: 纯函 谁借莪1个温暖的怀抱¢/ 2022年10月29日 09:26/ 0 赞/ 294 阅读
还没有评论,来说两句吧...