React:Redux 设计思想
Redux 背后的架构思想——认识 Flux 架构
Redux 的设计在很大程度上受益于 Flux 架构,可以认为 Redux 是 Flux 的一种实现形式(虽然它并不严格遵循 Flux 的设定),理解 Flux 将帮助更好地从抽象层面把握 Redux。
Flux 并不是一个具体的框架,它是一套由 Facebook 技术团队提出的应用架构,这套架构约束的是应用处理数据的模式。在 Flux 架构中,一个应用将被拆分为以下 4 个部分。
- View(视图层):用户界面。该用户界面可以是以任何形式实现出来的,React 组件是一种形式,Vue、Angular 也完全 OK。Flux 架构与 React 之间并不存在耦合关系。
- Action(动作):也可以理解为视图层发出的“消息”,它会触发应用状态的改变。
- Dispatcher(派发器):它负责对 action进行分发。
- Store(数据层):它是存储应用状态的“仓库”,此外还会定义修改状态的逻辑。store的变化最终会映射到 view层上去。
这 4 个部分之间的协作将通过下图所示的工作流规则来完成配合:
一个典型的 Flux 工作流是这样的:用户与 View
之间产生交互,通过 View
发起一个 Action
;Dispatcher
会把这个 Action
派发给 Store
,通知 Store
进行相应的状态更新。Store
状态更新完成后,会进一步通知 View
去更新界面。
值得注意的是,图中所有的箭头都是单向的,这也正是 Flux 架构最核心的一个特点——单向数据流
Flux 架构到底解决了什么问题
Flux 的核心特征是单向数据流,要想完全了解单向数据流的好处,我们需要先了解双向数据流带来了什么问题。
MVC 模式在前端场景下的局限性
双向数据流最为典型的代表就是前端场景下的 MVC 架构,该架构的示意图如下图所示:
除了允许用户通过 View 层交互来触发流程以外,MVC 架构还有另外一种形式,即允许用户通过直接触发 Controller
逻辑来触发流程,这种模式下的架构关系如下图所示:
在 MVC 应用中,会涉及这 3 个部分:
- Model(模型),程序需要操作的数据或信息;
- View(视图),用户界面;
- Controller(控制器),用于连接 View 和 Model,管理 Model与 View之间的逻辑。
原则上来说,三者的关系应该像上图一样,用户操作 View后,由 Controller来处理逻辑(或者直接触发 Controller的逻辑),经过 Controller将改变应用到 Model中,最终再反馈到 View上。在这个过程中,数据流应该是单向的。
事实上,在许多服务端的 MVC 应用中,数据流确实能够保持单向。但是在前端场景下,实际的 MVC 应用要复杂不少,前端应用/框架往往出于交互的需要,允许 View 和 Model 直接通信。此时的架构关系就会变成下图这样:
这就允许了双向数据流的存在。当业务复杂度较高时,数据流会变得非常混乱,出现类似下图这种情况:
图中我们的示例只有一个 Controller,但考虑到一个应用中还可能存在多个 Controller,实际的情况应该比上图还要复杂得多(尽管图示本身已经够复杂了)。
在如此复杂的依赖关系下,再小的项目变更也将伴随着不容小觑的风险——或许一个小小的改动,就会对整个项目造成“蝴蝶效应”般的巨大影响。如此混乱的修改来源,将会使得我们连 Bug 排查都无从下手,因为你很难区分出一个数据的变化到底是由哪个 Controller或者哪个 View 引发的。
此时再回头看下 Flux 的架构模式,你应该多少能感受到其中的妙处。这里再来回顾一下 Flux 中的数据流模式,请看下图:
Flux最核心的地方在于严格的单向数据流,在单向数据流下,状态的变化是可预测的。如果 store中的数据发生了变化,那么有且仅有一个原因,那就是由 Dispatcher派发 Action来触发的。这样一来,就从根本上避免了混乱的数据关系,使整个流程变得清晰简单。
不过这并不意味着 Flux 是完美的。事实上,Flux 对数据流的约束背后是不可忽视的成本:除了开发者的学习成本会提升外,Flux 架构还意味着项目中代码量的增加。
Flux 架构往往在复杂的项目中才会体现出它的优势和必要性。如果项目中的数据关系并不复杂,其实完全轮不到 Flux 登场,这一点对于 Redux 来说也是一样的。
Redux 是 JavaScript 状态容器,它提供可预测的状态管理。
Redux 关键要素与工作流回顾
虽然 Redux 在实现层面并没有按照 Flux 那一套来(比如 Flux 中允许多个 Store 存在,而 Redux 中只有一个 Store 等),但 Redux 在设计思想上确实和 Flux 一脉相承。
接下来介绍 Redux 的实现原理之前,先简单回顾一下它的关键要素与工作流。Redux 主要由 3 部分组成:Store、Reducer和 Action。
Store
:它是一个单一的数据源,而且是只读的。Action
:是“动作”的意思,它是对变化的描述。Reducer
:它负责对变化进行分发和处理,最终将新的数据返回给 Store。
Store
、Action
和 Reducer
三者紧密配合,便形成了 Redux
独树一帜的工作流,如下图所示:
在 Redux的整个工作过程中,数据流是严格单向的。如果你想对数据进行修改,只有一种途径:派发 Action。Action会被 Reducer读取,Reducer将根据 Action内容的不同执行不同的计算逻辑,最终生成新的 state(状态),这个新的 state会更新到 Store对象里,进而驱动视图层面作出对应的改变。
对于组件来说,任何组件都可以以约定的方式从 Store读取到全局的状态,任何组件也都可以通过合理地派发 Action来修改全局的状态。Redux通过提供一个统一的状态容器,使得数据能够自由而有序地在任意组件之间穿梭。
Redux 是如何工作的
先来看一下 Redux 的源码文件夹结构,如下图所示:
其中,utils是工具方法库;index.js作为入口文件,用于对功能模块进行收敛和导出。真正“干活”的是功能模块本身,也就是下面这几个文件:
- applyMiddleware.js
- bindActionCreators.js
- combineReducers.js
- compose.js
- createStore.js
applyMiddleware是中间件模块,它的独立性较强。
bindActionCreators(用于将传入的 actionCreator与 dispatch方法相结合,揉成一个新的方法)、combineReducers(用于将多个 reducer合并起来)、compose(用于把接收到的函数从右向左进行组合)这三个方法均为工具性质的方法。
不用急着去搜索这三个工具方法,因为它们均独立于 Redux 主流程之外,属于“非必须使用”的辅助 API,不熟悉这些 API 并不影响你理解 Redux 本身。理解 Redux 实现原理,真正需要我们关注的模块其实只有一个——createStore。
createStore方法是我们在使用 Redux 时最先调用的方法,它是整个流程的入口,也是 Redux 中最核心的 API。
故事的开始:createStore
使用 Redux 的第一步,我们就需要调用 createStore
方法。单纯从使用感上来说,这个方法做的事情似乎就是创建一个 store
对象出来,像这样:
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(reducer,initial_state,applyMiddleware(middleware1, middleware2, ...)
);
createStore
方法可以接收以下 3 个入参:
- reducer
- 初始状态内容
- 指定中间件
从拿到入参到返回出 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;// 记录当前的 statelet currentState = preloadedState;// 声明 listeners 数组,这个数组用于记录在 subscribe 中订阅的事件let currentListeners = [];// nextListeners 是 currentListeners 的快照let nextListeners = currentListeners;// 该变量用于记录当前是否正在进行 dispatchlet 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 中调用 subscribeif (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 可以更改当前的 reducerfunction 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内部逻辑总结进一张大图中,这张图涵盖了每个核心方法的工作内容,它将帮助快速把握 createStore的逻辑框架。
在 createStore
导出的方法中,与 Redux
主流程强相关的,同时也是我们平时使用中最常打交道的几个方法,分别是:
- getState
- subscribe
- dispatch
其中 getState
的源码内容比较简单,在逐行分析的过程中已经对它有了充分的认识。而 subscribe
和 dispatch
则分别代表了 Redux
独有的“发布-订阅”模式以及主流程中最为关键的分发动作。
针对 dispatch
和 subscribe
这两个具体的方法进行分析,分别认识 Redux
工作流中最为核心的dispatch
动作,以及 Redux
自身独特的 “发布-订阅”模式。
Redux 工作流的核心:dispatch 动作
dispatch
应该是大家在使用 Redux
的过程中最为熟悉的 API 了。结合前面对设计思想的解读,我们已经知道,在 Redux
中有这样 3 个关键要素:
- action
- reducer
- store
之所以说 dispatch是 Redux 工作流的核心,是因为dispatch 这个动作刚好能把 action、reducer和 store这三位“主角”给串联起来。dispatch的内部逻辑,足以反映了这三者之间“打配合”的过程。
这里把 dispatch的逻辑从 createStore中提取出来,请看相关源码:
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,计算新的 statecurrentState = currentReducer(currentState, action)} finally {// 执行结束后,把"锁"打开,允许再次进行 dispatchisDispatching = false}// 触发订阅const listeners = (currentListeners = nextListeners);for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];listener();}return action;
}
结合源码,将 dispatch
的工作流程提取如下:
在这段工作流中,有两个点值得细细回味。
1. 通过“上锁”避免“套娃式”的 dispatch
dispatch
工作流中最关键的就是执行 reducer
这一步,它对应的是下面这段代码:
try {// 执行 reducer 前,先“上锁”,标记当前已经存在 dispatch 执行流程isDispatching = true// 调用 reducer,计算新的 state currentState = currentReducer(currentState, action)
} finally {// 执行结束后,把"锁"打开,允许再次进行 dispatch isDispatching = false
}
这里之所以要用 isDispatching将 dispatch的过程锁起来,目的是规避“套娃式”的 dispatch。更准确地说,是为了避免开发者在 reducer中手动调用 dispatch。
因此,在 dispatch的前置校验逻辑中,一旦识别出 isDispatching为 true,就会直接 throw Error(见下面代码),把死循环扼杀在摇篮里:
if (isDispatching) {throw new Error('Reducers may not dispatch actions.')
}
2. 触发订阅的过程
在 reducer
执行完毕后,会进入触发订阅的过程,它对应的是下面这段代码:
// 触发订阅
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];listener();
}
- 之前并没有介绍 subscribe这个 API,也没有提及 listener相关的内容,它们到底是如何与 Redux 主流程相结合的呢?
- 为什么会有 currentListeners和 nextListeners这两个 listeners数组?这和我们平时见到的“发布-订阅”模式好像不太一样。
function handleChange() {// 函数逻辑
}
const unsubscribe = store.subscribe(handleChange)
unsubscribe()
subscribe在订阅时只需要传入监听函数,而不需要传入事件类型。这是因为 Redux 中已经默认了订阅的对象就是“状态的变化(准确地说是 dispatch 函数的调用)”这个事件。
接下来我们结合源码来分析一下 subscribe的内部逻辑,subscribe源码提取如下:
function subscribe(listener) {// 校验 listener 的类型if (typeof listener !== 'function') {throw new Error('Expected the listener to be a function.')}// 禁止在 reducer 中调用 subscribeif (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);};
}
结合这段源码,可以将 subscribe
的工作流程提取如下:
要理解这个问题,首先要搞清楚 Redux 中的订阅过程和发布过程各自是如何处理 listeners数组的。
1. 订阅过程中的 listeners 数组
let nextListeners = currentListeners
function ensureCanMutateNextListeners() {// 若两个数组指向同一个引用if (nextListeners === currentListeners) {// 则将 nextListeners 纠正为一个内容与 currentListeners 一致、但引用不同的新对象nextListeners = currentListeners.slice()}
}
nextListeners.push(listener);
2. 发布过程中的 listeners 数组
触发订阅这个动作是由 dispatch
来做的,相关的源码如下:
// 触发订阅
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];listener();
}
3. currentListeners 数组用于确保监听函数执行过程的稳定性
正因为任何变更都是在 nextListeners
上发生的,我们才需要一个不会被变更的、内容稳定的 currentListeners
,来确保监听函数在执行过程中不会出幺蛾子。
// 定义监听函数 A
function listenerA() {}
// 订阅 A,并获取 A 的解绑函数
const unSubscribeA = store.subscribe(listenerA)
// 定义监听函数 B
function listenerB() {// 在 B 中解绑 AunSubscribeA()
}
// 定义监听函数 C
function listenerC() {}
// 订阅 B
store.subscribe(listenerB)
// 订阅 C
store.subscribe(listenerC)
在这个 Demo 执行完毕后,nextListeners
数组的内容是 A、B、C 3 个 listener
:
[listenerA, listenerB, listenerC]
接下来若调用 dispatch
,则会执行下面这段触发订阅的逻辑:
// 触发订阅
const listeners = (currentListeners = nextListeners);
for (let i = 0; i < listeners.length; i++) {const listener = listeners[i];listener();
}
return function unsubscribe() {// 避免多次解绑if (!isSubscribed) {return;}isSubscribed = false;// 熟悉的操作,调用 ensureCanMutateNextListeners 方法ensureCanMutateNextListeners();// 获取 listener 在 nextListeners 中的索引const index = nextListeners.indexOf(listener);// 将当前的 listener 从 nextListeners 数组中删除 nextListeners.splice(index, 1);
};
[listenerB, listenerC]
在示例的这种场景下,ensureCanMutateNextListeners执行前,listeners、currentListeners和 nextListeners之间的关系是这样的:
listeners === currentListeners === nextListeners
而 ensureCanMutateNextListeners
执行后,nextListeners
就会被剥离出去:
nextListeners = currentListeners.slice()
listeners === currentListeners !== nextListener
React:Redux 设计思想相关推荐
- 应用数据流状态管理框架Redux简介、设计思想、核心概念及工作流
tip:有问题或者需要大厂内推的+我脉脉哦:丛培森 ٩( 'ω' )و [本文源址:http://blog.csdn.net/q1056843325/article/details/54784109 ...
- 一个 react+redux 工程实例
在前几天的一篇文章中总结部分提到了学习过程中基础的重要性.当然,并不是不支持大家学习新的框架,这篇文章就分享一下react+redux工程实例. 一直在学习研究react.js,前前后后做了几次分享. ...
- React+Redux打造“NEWS EARLY”单页应用 一步步让你理解最前沿技术栈的真谛
之前写过一篇文章,分享了我利用闲暇时间,使用React+Redux技术栈重构的百度某产品个人中心页面.您可以参考这里,或者参考Github代码仓库地址. 这个工程实例中,我采用了厂内的工程构建工具-F ...
- [Redux/Mobx] Mobx的设计思想是什么
[Redux/Mobx] Mobx的设计思想是什么 依赖收集.在Mobx中,定义了observable的属性,mobx会自动跟踪这个属性值的变化:在用了mobx与react的桥接库mobx-react ...
- React/React Native框架的设计思想
React Native框架的编程思想 (一)React Native框架的设计思想 基于响应式编程范式 从其全局刷新的机制以及flux架构可以得出,react native是基于响应式编程范式的产物 ...
- 前端React教程第二课 React生命周期设计思想
02 为什么 React 16 要更改组件的生命周期?(上) React 生命周期已经是一个老生常谈的话题了,几乎没有哪一门 React 入门教材会省略对组件生命周期的介绍.然而,入门教材在设计上往往 ...
- React 设计思想
React 设计思想 译者序:本文是 React 核心开发者.有 React API 终结者之称的 Sebastian Markbåge 撰写,阐述了他设计 React 的初衷.阅读此文,你能站在更高 ...
- React+Redux+中间件
MVVM是Model-View-ViewModel的缩写.mvvm是一种设计思想.Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑:View 代表UI 组件,它负责将数据模 ...
- React组件设计实践总结05 - 状态管理
今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案. 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? 还是错别字太多了? 后面静 ...
最新文章
- ylb:使用sql语句实现添加、删除约束
- layer iframe层的使用,传参
- Binary classification - 聊聊评价指标的那些事儿【实战篇】
- java压缩传输_简单实现字符串的压缩,减轻传输压力
- hibernate mysql 配置文件_hibernate 框架的配置文件和映射文件以及详解
- ExtJs使用自定义插件动态保存表头配置(隐藏或显示)
- linux 6.4 multipath.conf跟其他版本的区别,宏杉与其他厂商存储共用multipath的配置方法...
- python构建电商用户画像(1)
- 猫盘onespace x3p系统使用
- 开淘宝店怎么注册公司?开淘宝店是否需要去工商局登记注册公司
- 系统分析与控制_多智能体协同控制研究中各定位系统分析
- Linux常用命令——mysqladmin命令
- Python爬虫基础-mysql数据库
- Shell知识点(一)基本语法
- 常用链接ssh服务器的工具(推荐)
- ultra fast lane detection数据集制作
- Debian11(Bullseye)系统安装docker及启动失败问题解决
- c语言位段实现字节异或,C语言-位运算-小结
- 读书笔记(被讨厌的勇气一)
- Q3净收入创单季新高,每日优鲜靠什么增速增效?
热门文章
- 母函数详解(转 侵删)
- 程序设计之HardCoding
- 告诉你一个朴素的上海(中/食)
- Win10自带的SSH客户端
- 《工作前5年,决定你一生的财富》三公子TXT,PDF,epub,mobi,azw3,kindle电子书下载
- java压测服务器_Java简单模拟设备压测服务器(Rabbitmq)
- 滑铁卢计算机专业qs排名,新鲜出炉 2021年滑铁卢大学世界综合及专业排名 很强势有木有!...
- C++项目新冠疫苗预约系统
- 安装mechanize
- Android小程序之音乐播放列表