阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

深入浅出 React(五):使用 Flux 搭建 React 应用程序架构

  • 2015-12-09
  • 本文字数:6203 字

    阅读完需:约 20 分钟

前面几篇文章介绍了 React 相关的基本概念和运行原理,可以看到 React 是一个完全面向 View 的解决方案,它让我们能以一种新的思路去实现 View,让很多复杂的场景可以用一种简单的方法去解决。然而在一个完整的应用程序中,除了实现 View 之外,我们还需要考虑如何同服务器通信、View 之间如何交互以及 View 背后的数据模型如何去设计。那么 Flux 正是 Facebook 提出的解决这些问题的方案。

简单来说,Flux 定义了一种单向数据流的方式,来实现 View 和 Model 之间的数据流动。它更像是一种设计模式而非一个正式的框架,以至于官方的 Flux 参考实现只有一个文件,区区 100 多行源代码。所以 Flux 继承了 React 的简单、直观的设计思想,让人一眼就能看明白其背后的运行原理。当然,要用好 Flux,还是要正确理解其概念和背后的出发点,官方则是提供了两个具体的例子供大家参考。

Flux 的标准实现非常简单,因此还衍生出了很多第三方实现,比较著名的包括 Redux,Reflux,Fluxmm。而如今最为火热的应该属于 Redux,它采用了函数式编程的思想来维护整个应用程序的状态。其实无论哪一种框架,都是以 Flux 的架构为基础而做的演变,其核心都是单向数据流和单向数据绑定。因而本文只会介绍官方的 Flux,理解了标准实现之后也会更容易理解其他的实现方式。大家可以按照自己的兴趣和认可程度选择最适合自己的实现。

Flux 要解决的问题

在传统 MVC 框架中,通常使用双向绑定的方式来将 Model 的数据展现到 View。当 Model 中的数据发生变化时,一个或多个 View 会发生变化;当 View 接受了用户输入时,Model 中的数据则会发生变化。在实际的应用中,当一个 Model 中的数据发生变化时,也有可能另一个相关的 Model 中的数据会被同步更新。这样,很容易出现的一个现象就是连锁更新(Cascading Update),Model 可以更新 Model,Model 可以更新 View,View 也可以更新 Model。你很难去推断一个界面的变化究竟是由哪个局部的功能代码引起。如下图所示, Model 和 View 之间的关系错综复杂,导致出现问题时很难调试;实现新功能时也需要时刻注意代码是否会产生副作用。

对此问题,Flux 的解决方案是让数据流变成单向,引入 Store、Action、Action Creators 和 Dispatcher 等概念来管理信息流。如下图所示:

可以看到,数据流变成单向的。同时,数据如何被处理也被明确的定义了。在 MVC 中,数据如何处理通常由 Controller 来完成,在 Controller 中实现大部分的业务逻辑来处理数据。而现在则被清晰的定义在 Store 或者 Action Creators 中。当然,上图隐藏了一些细节,更为全面的架构图则如下所示:

在 Flux 中,View 完全是 Store 的展现形式,Store 的更新则完全由 Action 触发。得益于 React 的 View 每次更新都是整体刷新的思路,我们可以完全不必关心 Store 的变化细节,只需要监听 Store 的 onChange 事件,每次变化都触发 View 的 re-render。从而也可以看到,尽管 Flux 架构可以离开 React 单独使用,但无疑两者结合是一个更加和谐的方案,能够各发挥所长。

看一个具体的例子

为了对 Flux 有一个总体的印象,我们先考虑一个简单的使用场景:在文章评论页面提交一条评论。为此,我们需要向服务器发送一个请求提交新的评论,同时要将新的评论显示在列表中。这样的场景如果使用 Flux 去实现,大概需要实现以下几个部分:

  1. React 组件用于显示评论列表以及评论框,并绑定到 Store;
  2. 一个 Store 用于存储评论数据;
  3. Action Creator 用于向服务器发送请求;
  4. Store 中监听 Action 并进行处理,从而对 Store 自身进行更新。

整个架构如下图所示:

(点击放大图像)

整个流程的运行大概如下:

  1. 用户点击提交按钮,Action Creator 负责向服务器发送请求;
  2. 请求如果成功,那么将评论本身被添加到 Store;
  3. 请求如果失败,那么在 Store 中标记一个特别的错误状态;
  4. View 监听了 Store 的 onChange 的事件,因此,无论请求成功和失败,Store 都会触发 onChange 事件,这时 View 就会进行整体更新。

可以看到,无论请求成功和失败,都是去修改组件之外的 Store,由 Store 通知 UI 进行变化。在这样一个架构中,Store 中存储的是整个或者一部分应用程序的状态,React 实现的 View 只需要监听 Store 的变化,而无需知道变化的细节,这也是由 React 组件的特点决定的。这样,我们就使用 Flux 完成了评论功能,不同于双向绑定,在 Flux 的流程中,数据如何流转和变化,变得非常清晰明确。虽然可能需要写更多的代码,但是带来了更清楚的架构。下面,我们来具体看其中的每个具体组件的概念和用法。

View 和 Store

在 Flux 架构中,View 即 React 的组件,而 Store 则存储的是应用程序的状态。在前面的文章中我们已经介绍过,React 是完全面向 View 的解决方案,它提供了一种始终都是整体刷新的思路来构建界面。在 React 的思路中,UI 就是一个状态机,每个确定的状态对应着一个确定的界面。对于一个小的组件,它的状态可能是在其内部进行维护;而对于多个组件组成的应用程序,如果某些状态需要在组件之间进行共享,则可以将这部分状态放到 Store 中进行维护。在 Flux 中,Store 并不是一个复杂的机制,甚至 Flux 的官方实现中并没有任何 Store 相关的机制和接口,而是仅仅通过示例来描述了一个 Store 应该是什么样的数据结构。例如,在官方提供的 TodoMVC 例子 ( https://github.com/facebook/flux/tree/master/examples/flux-todomvc/ ) 中,Store 的实现如下:

复制代码
var _todos = [];
var TodoStore = assign({}, EventEmitter.prototype, {
/**
* Get the entire collection of TODOs.
* @return {object}
*/
getAll: function() {
return _todos;
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});

可以看到,一个 Flux 的 Store 就是一个能触发 onChange 事件的对象,能够让其它对象订阅(addChangeListener)或者取消订阅(removeChangeListener)。同时,它提供了一些 API 供 View 来获取自己需要的状态。因此,也可以将 Store 理解为需要被不同 View 共享的公用状态。

那么,已经有了 Store,React 的组件(View)该如何使用它们呢?其实很简单,只需要在 Store 每次变化时都去获取一下最新的数据即可。我们可以看下 TodoMVC 中的实现:

复制代码
var TodoStore = require('../stores/TodoStore');
/**
* Retrieve the current TODO data from the TodoStore
*/
function getTodoState() {
return {
allTodos: TodoStore.getAll(),
areAllComplete: TodoStore.areAllComplete()
};
}
var TodoApp = React.createClass({
getInitialState: function() {
return getTodoState();
},
componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
TodoStore.removeChangeListener(this._onChange);
},
/**
* @return {object}
*/
render: function() {
return (
<div>
<Header />
<MainSection
allTodos={this.state.allTodos}
areAllComplete={this.state.areAllComplete}
/>
<Footer allTodos={this.state.allTodos} />
</div>
);
},
/**
* Event handler for 'change' events coming from the TodoStore
*/
_onChange: function() {
this.setState(getTodoState());
}
});

可以看到,在组件的 componentDidMount 方法中,开始监听 Store 的 onChange 事件,在 componentWillUnmount 方法中,取消监听 onChange 事件。在 Store 的每次变化后,都去重新获取自己需要的状态数据:getTodoState()。

通过这样一种很简单的机制,我们建立了从 Store 到 View 的数据绑定,每当 Store 发生变化,View 也会进行相应的更新。那么底下我们需要关心当 View 接收用户交互,需要将新的状态存入到 Store 中,应该如何去实现。这就需要引入 Flux 的另外两个概念 Dispatcher 和 Action。

Dispatcher,Action

顾名思义,Dispatcher 就是负责分发不同的 Action。在一个 Flux 应用中,只有一个中心的 Dispatcher,所有的 Action 都通过它来分发。而 Facebook 的官方 Flux 实现其实就仅仅是提供了 Dispatcher。使用 Dispatcher 只需要将其作为 npm 模块引入:

var Dispatcher = require('flux').Dispatcher;典型的,Dispatcher 有两个方法:

  1. dispatch:分发一个 Action;
  2. register:注册一个 Action 处理函数。

这样,当 View 接受了一个用户的输入之后,通过 Dispatcher 来分发一个特定的 Action,而对应的 Action 处理函数会负责去更新 Store。这个流程在文章开始的图中可以清楚的看到。因此,通常来说 Action 的处理函数会和 Store 放在一起,因为 Store 的更新都是由 Action 处理函数来完成的。例如在 TodoMVC 中,TodoStore 中会处理如下 Action:

复制代码
Dispatcher.register(function(action) {
var text;
switch(action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if (text !== '') {
create(text);
TodoStore.emitChange();
}
break;
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
if (TodoStore.areAllComplete()) {
updateAll({complete: false});
} else {
updateAll({complete: true});
}
TodoStore.emitChange();
break;
case TodoConstants.TODO_UNDO_COMPLETE:
update(action.id, {complete: false});
TodoStore.emitChange();
break;
case TodoConstants.TODO_COMPLETE:
update(action.id, {complete: true});
TodoStore.emitChange();
break;
case TodoConstants.TODO_UPDATE_TEXT:
text = action.text.trim();
if (text !== '') {
update(action.id, {text: text});
TodoStore.emitChange();
}
break;
case TodoConstants.TODO_DESTROY:
destroy(action.id);
TodoStore.emitChange();
break;
case TodoConstants.TODO_DESTROY_COMPLETED:
destroyCompleted();
TodoStore.emitChange();
break;
default:
// no op
}
});

无论是添加、删除还是修改一个 Todo 项,都是由 Action 来触发的。在 Action 处理函数中,不仅对 Store 进行了更新,还触发了 Store 的 onChange 事件,从而让所有监听组件能够得到通知。完整的代码可以参考: https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/stores/TodoStore.js

通过 Dispatcher 和 Action,实现了从 View 到 Store 的数据流,进而实现了整个 Flux 的单向数据流循环。从这里可以看到,Dispatcher 是全局唯一的,相当于是所有 Action 的总 hub,而每个 Action 处理函数都能够收到所有的 Action,至于需要对哪些进行处理,则由处理函数自己决定。例子中是通过 switch 来判断 Action 的 type 属性来决定如何进行处理。因此,虽然不是必须,但是一般 Action 都会有一个 type 属性来标识其类型。

Action Creators

有了上述概念和机制,基本上就已经有了 Flux 的整个架构的模型。那么 Action Creators 又是什么呢?顾名思义,Action Creators 即 Action 的创建者。它们负责去创建具体的 Action。一个 Action 可以由一个界面操作产生,也可以由一个 Ajax 请求的返回结果产生。为了让这部分逻辑更加清晰,让 View 更少的去关心数据流的细节,于是有了 Action Creators。例如,对于一个 TodoItem 组件,当用户点击其中的 Checkbox 时,会产生一个 COMPLETE_TODO 的 Action,直观的看,完全可以在 View 的内部去实现 Dispatcher.dispatch({type: ‘COMPLETE_TODO’, payload: {…}),而为了保持 View 的简单和直观,通常会在独立的 Action Creators 去封装这部分逻辑,例如:

复制代码
var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');
var TodoActions = {
/**
* @param {string} text
*/
create: function(text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
/**
* @param {string} id The ID of the ToDo item
* @param {string} text
*/
updateText: function(id, text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_UPDATE_TEXT,
id: id,
text: text
});
},
}

这里的 TodoActions 就是一个 Action Creator,它把具体分发 Action 的动作封装成具有语义的方法,供 View 去使用,那么在一个 TodoItem 的组件中,其界面 JSX 可能就是:

复制代码
render: function() {
var todo = this.props.todo;
var input;
if (this.state.isEditing) {
input =
<TodoTextInput
className="edit"
onSave={this._onSave}
value={todo.text}
/>;
}
return (
<li
className={classNames({
'completed': todo.complete,
'editing': this.state.isEditing
})}
key={todo.id}>
<div className="view">
<input
className="toggle"
type="checkbox"
checked={todo.complete}
onChange={this._onToggleComplete}
/>
<label onDoubleClick={this._onDoubleClick}>
{todo.text}
</label>
<button className="destroy" onClick={this._onDestroyClick} />
</div>
{input}
</li>
);
},
_onToggleComplete: function() {
TodoActions.toggleComplete(this.props.todo);
},

通过将 Action 的创建逻辑放到 Action Creators,可以让 View 更加简单和纯粹,View 不需要知道背后是否有 Flux,而只需要知道调用某个方法来实现某个功能,从而让 View 的开发更加流畅和直观 。完整代码可以参考: https://github.com/facebook/flux/blob/master/examples/flux-todomvc/js/components/TodoItem.react.js

小结

本文介绍了 Facebook 提出的面向 React 的一种新的应用架构模式 Flux,这种架构也已经在 Facebook 内部被广泛使用,其概念和原理虽然很简单直观,但是确实被证明有能力去组织一个完整的大型应用。然而 Flux 的中心 Dispatcher 的模式,以及 Action 作为全局可知的数据流的方式,仍然有一些争论。因此,社区中也是出现了非常多的类 Flux 实现,如今最成熟的莫过于 Redux,其最大的特点在于 Action 能够分层次的去负责一个全局 Store 的不同部分,从而更容易去模块化应用状态的管理。理解了本文介绍的 Flux 概念,对于理解 Redux 也会有很大的帮助。


感谢徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群(已满),InfoQ 读者交流群(#2))。

2015-12-09 16:2913439

评论

发布
暂无评论
发现更多内容

SAP Cloud for Customer Extensibility的设计与实现

Jerry Wang

SAP abap C4C 11月日更

「Oracle」Oracle 数据库安装

恒生LIGHT云社区

数据库 oracle

关于HTTPS认证,这里解决你所有疑惑

华为云开发者联盟

https 证书 数据加密 认证 签发证书

搞定大厂算法面试之leetcode精讲4.贪心

全栈潇晨

LeetCode 算法面试

OpenHarmony驱动框架解读和开发实践|HDC2021 技术分论坛

HarmonyOS开发者

HarmonyOS

吐司盒子?芝士码?HarmonyOS创新音视频测试技术来啦|HDC2021技术分论坛

HarmonyOS开发者

HarmonyOS

模块四作业

Geek_1d37ea

架构训练营

HarmonyOS内核技术大揭秘|HDC2021技术分论坛

HarmonyOS开发者

HarmonyOS

DevEco Testing,新增分布式测试功能|HDC2021技术分论坛

HarmonyOS开发者

HarmonyOS

dart系列之:在dart中使用数字和字符串

程序那些事

flutter dart 程序那些事 11月日更

“愚公移山”的方法解atoi,自以为巧妙!

老表

Python LeetCode 11月日更 算法与数据结构

Junit 4 的 @Before 和 @BeforeClass 对比 Junit 5 @BeforeEach 和 @BeforeAll

HoneyMoose

Hibernate H2 数据库连接配置 URL 解读

HoneyMoose

Chrome 插件特性及实战场景案例分析

vivo互联网技术

大前端 插件设计 chrome扩展

奖金翻倍!Flink Forward Asia Hackathon 最新参赛指南请查收

Apache Flink

大数据 flink 编程 后端 hackathon

模块四学习总结

Geek_1d37ea

架构训练营

一次谈不上有点内卷的美东某金融公司面试

HoneyMoose

MySQL Workbench 使用教程 - 如何使用 Workbench 操作 MySQL / MariaDB 数据库中文指南

蒋川

MySQL MariaDB MySQL 数据库

linux下prometheus+grafana安装

小鲍侃java

11月日更

筹备两年,60万字诚意续作《腾讯游戏开发精粹Ⅱ》正式发布

博文视点Broadview

Sechunter移动应用隐私合规检测详解

华为云开发者联盟

移动应用 目标检测 隐私 Sechunter 隐私合规

IntelliJ IDEA 如何针对 Java 项目创建 H2 数据库连接

HoneyMoose

云原生社区上线了

云原生

开源 云原生 技术社区 社区

创建第一个微信小程序

坚果

微信小程序 11月日更

数仓如何限制临时数据文件下盘量

华为云开发者联盟

sql 线程 GaussDB(DWS) 临时文件 落盘

如何让 Sublime Text 编辑器支持新的 ABAP 关键字

Jerry Wang

SAP abap 11月日更 Sublime

填坑总结:python内存泄漏排查小技巧

华为云开发者联盟

Python 内存 内存泄漏 回收 全局变量

又添权威认定,旺链科技通过可信区块链专项认证!

旺链科技

区块链 产业区块链 技术测评 数字化经济

应用不停机发布的思考与初识

陈俊

高可用 技术架构 不停机发布

openGauss开源自动化测试框架Yat,增强社区测试能力

openGauss

压缩比达到7:1,TDengine助力校园智慧用电系统降本增效

TDengine

tdengine 时序数据库

深入浅出React(五):使用Flux搭建React应用程序架构_语言 & 开发_王沛_InfoQ精选文章