AI实践哪家强?来 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:2914189

评论

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

什么!阿里最新版Spring Cloud Alibaba项目文档,竟将重要组件弃用

收到请回复

Java spring 阿里巴巴 面试 spring-cloud

一对一直播软件——如何实现音视频传播?

开源直播系统源码

软件开发 直播系统源码 一对一语音聊天软件 语音直播系统

对话ACE第五期:到底什么才是真正的HTAP?

OceanBase 数据库

私有化部署的低代码平台 更安全的信息化解决方案

力软低代码开发平台

20万字的《Kafka运维实战宝典》PDF现在免费下载了

石臻臻的杂货铺

大数据 kafka

多并发下线程创建、释放的阻塞问题

TiDB 社区干货传送门

HarmonyOS Connect FAQ第四期

HarmonyOS开发者

HarmonyOS

利用现有数据库管理系统创建一个安全的分布式数据库集群

亚马逊云科技 (Amazon Web Services)

大数据 分布式 Tech 专栏

采访22年第一批秋招上岸的同学后,我整理了这份Java面试手册

Java面试那些事儿

Java 编程 程序员 架构 面试

离线部署系列文章之二:TiDB集群升级(5.3.0->5.4.2)&缩扩容 TiDB Server、PD、TiKV、TiFlash

TiDB 社区干货传送门

版本升级 集群管理 管理与运维 安装 & 部署

PingCAP Clinic 服务:贯穿云上云下的 TiDB 集群诊断服务

TiDB 社区干货传送门

基础到高级涵盖11个技术,Alibaba最新出品711页Java面试神册真香

收到请回复

Java 大数据 架构 编程语言 语言 & 开发

传统堡垒机数据可以迁移到云堡垒机上吗?方式有哪些?

行云管家

云计算 网络安全 堡垒机

云堡垒机主要针对运维过程中的什么进行管理和审计?

行云管家

运维 堡垒机 IT运维 云堡垒机

Go-Excelize API源码阅读(十四)——GetSheetFormatPr

Regan Yue

开源 源码刨析 Go 语言 8月日更 8月月更

2022 OceanBase 年度发布会:发布四大策略,迈入4.0时代

OceanBase 数据库

故障处理 | DM 搭建 MySQL 8.0 同步链路报错:code=26005

TiDB 社区干货传送门

安装 & 部署 TiDB 源码解读

如何在 TiDB Cloud 上使用 Databricks 进行数据分析 | TiDB Cloud 使用指南

TiDB 社区干货传送门

TiDB 和 Java 的简单 CRUD 应用程序

TiDB 社区干货传送门

手把手教你实现 TiFlash 向量化函数丨十分钟成为 TiFlash Contributor

TiDB 社区干货传送门

如何让 TiDB 集群管理“更省心”?TiUniManager(原 TiEM)使用教程来了

TiDB 社区干货传送门

大数据训练营毕业总结

Geek_Q

离线部署系列文章之一:TiDBv5.3.0集群部署&源码部署 Haproxy v2.5.0

TiDB 社区干货传送门

实践案例 版本升级 管理与运维 安装 & 部署 扩/缩容

TiDB 和 Golang 的简单 CRUD 应用程序

TiDB 社区干货传送门

五天玩转EMAS Serverless

云端explorer

云计算 Serverless emas

TiFlash Proxy 模块介绍

TiDB 社区干货传送门

希捷亮相OCP China Day 2022,与生态伙伴共话绿色存储之道

极客天地

魅族高校新生充电计划进行中,直播课让科目一新生直呼厚道

极客天地

某站下载量过W的近4000页“Java面试合集”号称大厂面试零门槛

收到请回复

Java 程序员 面试 金九银十

膜拜阿里!首次发布「10亿级并发系统设计文档」(内部绝密)

退休的汤姆

阿里 面经 Java工程师 秋招 并发系统设计

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