AICon议程上新60%,阿里国际、360智脑、科大讯飞、蔚来汽车分享大模型探索与实践 了解详情
写点什么

深入浅出 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:2913760

评论

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

如何进行数据挖掘?

郑州埃文科技

数据挖掘 数据库

企业知识管理的目标是什么?

小炮

Gitlab-ci 替代 webhook 触发Jenkins job

网易云信

gitlab

【IT运维】多台海外主机运维用什么工具好?

行云管家

服务器 IT运维 服务器运维 海外主机

大数据培训:Hadoop和MPP有什么区别

@零度

hadoop MPP 大数据开发

Go HTTP Server 基于OpenTelemetry 使用Jaeger - 代码实操

非晓为骁

Go Docker Trace Jaeger OpenTelemetry

移动域全链路可观测架构和关键技术

阿里巴巴终端技术

架构 App 移动端 体验优化

JavaScript深入理解之闭包

锋享前端

小白入门HarmonyOS Connect设备开发的“芯”路历程

HarmonyOS开发者

芯片 HarmonyOS 设备

三级等保是最高的吗?有什么用?

行云管家

网络安全 等保 等保2.0

【直播回顾】OpenHarmony知识赋能第四期直播——标准系统HDF开发

OpenHarmony开发者

直播 HDF OpenHarmony

数字化时代下,智能运维全栈监控解决方案及案例盘点

云智慧AIOps社区

运维 解决方案 场景应用 自动化运维 运维安全

Java基础系列文章---异常

NoLongerConfused

3月月更

java培训:SpringBoot高频面试考点分享

@零度

JAVA开发 springboot

Jaeger docker部署实操

非晓为骁

Docker Jaeger Go 语言 http client

实用机器学习笔记二十五:超参数优化

打工人!

学习笔记 超参数调优 机器学习算法 3月月更

低代码实现探索(三十七)业务的流程,开发的框架

零道云-混合式低代码平台

TiDB 可观测性方案落地探索 | “我们这么菜评委不会生气吧”团队访谈

PingCAP

WebRTC 简单入门

ZEGO即构

WebRTC 动手实践 音视频开发 即构科技

向工程腐化开炮 | Java代码治理

阿里巴巴终端技术

Java android JVM 代码治理

web前端培训:react高频面试题分享

@零度

前端开发 React

中国AI的下一站:从两会高地奔涌向产业河谷

脑极体

【ELT.ZIP】OpenHarmony啃论文俱乐部——多维探秘通用无损压缩

ELT.ZIP

OpenHarmony 压缩算法

虎符交易所HOO持续创造今年新高,你的HOO囤够了吗?

区块链前沿News

加密资产 Hoo 虎符交易所 平台币

浏览器工作原理和V8引擎

CRMEB

昇思MindSpore全场景AI框架 1.6版本,更高的开发效率,更好地服务开发者

Geek_32c4d0

mindspore 昇思 全场景AI框架

N个技巧,编写更高效 Dockerfile|云效工程师指北

阿里云云效

阿里云 云原生 Dockerfile 部署与维护 构建工具

ICASSP 2022 | 前沿音视频成果分享:基于可变形卷积的压缩视频质量增强网络

阿里云视频云

阿里云 计算机视觉 音视频 视频编码 视频云

【51单片机】室友用一把王者时间,学会了去使用数码管

謓泽

3月月更

打造优质的车联网体验,仍需注意数据安全保护

FinClip

数据预处理和特征选择

云智慧AIOps社区

数据挖掘 机器学习 算法 特征选择 数据预处理

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