Flux架构

当应用复杂程度增加时,state会变得越来越复杂,只用React开发将会变得力不从心。不仅仅是view层的内容,还有其他的比如说数据流向、state管理、路由解决方案等。React的开发者推出了Flux机构及其官方实现。同时业内也推出了很多其他的Flux实现,其中又以Redux这个库为翘楚。
  Flux是Facebook官方提出的一套前端应用架构模式。核心是单向数据流。它更像一种软件开发模式,而不是具体的一个框架,所以基于Flux存在很多的实现。

  • 单向数据流
    单向数据流是Flux的核心。读者可能接触过MVC这种软件架构,它的数据流动是双向的。controller是model和view之间交互的媒介,它要处理view的交互操作,通知model进行更新,同时在操作成功后通知view更新。这种双向的模式在model和view的对应关系变得越来越复杂的时候,就会遇到很多困难,难以维护和调试。

  • Flux流程


    Flux单向数据流
  • Action:是用来描述单个行为的对象,比如说创建文章的Action可以是{actionName: 'createPost', data: {'content': 'new stuff'}}。

  • Dispatcher:Dispatcher是信息分发中心,是Action和Store的连接中心,Dispatcher可以使用dispatch方法执行一个Action,并且可以用register方法注册回调,在回调方法中处理Store的内容。

  • Store:Store处理完毕后,它可以使用emit方法向其他地方发送命名为change的广播,告知他妈Store已经发生变更。

  • View:View层监着change事件,一旦change事件被触发,那么该层可以调用setState来更新整个UI。

Flux架构示例

现在用Flux架构完成一个Todo代办事项的小程序。

  • 项目结构
components/
  --Todo.jsx(程序框架)
  --List.jsx(代办事项列表)
  --CreateButton.jsx(新建代办事项按钮)
actions/
  --TodoAction.js(程序中所有的action)
dispatcher/
  --AppDispatcher.js(程序中的总调度)
stores
  --TodoStore.js(管理程序中的数据的存放)
  • Dispatcher和action

可以把Dispatcher看作是一个调度中心,把action看作是应用程序的各种交互动作,而每个动作产生后都会交给Dispatcher这个调度中心来处理。Dispatcher有Facebook的官方实现,称为Flux Dispathcer:

// dispatcher/AppDispatcher.js
// 实例化一个Dispatcher并返回
import { Dispatcher } from 'flux';

export default new Dispatcher();

新建或删除一个Todo都会产生一个action:

// Todo.jsx
...
import TodoAction from '.../actions/TodoAction';
...
class Todo extends React.Component {
  constructor(props) {
    this.createTodo = this.createTodo.bind(this);
    this.deleteTodo = this.deleteTodo.bind(this);
  }
  createTodo() {
    // 创建Todo的事件回调
    TodoAction.create({ id: uuid.v4(), content: '3rd stuff' });
  }
  deleteTodo(id) {
    // 删除Todo的事件回调
    TodoAction.delete(id);
  }
  render() {
    return (
      <div>
        <List items={this.state.todos} onDelete={this.deleteTodo} />
        <CreateButton onClick={this.createTodo} />
      </div>
    );
  }
}

export default Todo;

当按钮被单击时,一个特殊的action会被触发,并交给Dispatcher处理:

// ./actions/TodoAction
import AppDispatcher from '../dispatcher/AppDispatcher';

const TodoAction = {
  create(todo) {
    AppDispatcher.dispatch({
      actionType: 'CREATE_TODO',
      todo
    });
  },
  delete(id) {
    AppDispatcher.dispatch({
      actionType: 'DELETE_TODO',
      id
    });
  }
};

export default TodoAction;

action只不过是一个普通的JavaScript Object,用一个actionType字段表明用途,另外一个字段表明它传递的信息。
在这里,dispatch的是一个对象,但是当应用复杂程度不断增加的时候,就可能在不同的view中dispatch相同的对象,而且必须有着相同的actionType,还要记牢数据的格式,这都不利于代码复用,所以Flux提出了一个新的概念,称为action creator,其实就是把这些数据抽象到一些函数中。就像在TodoAction里面写的一样:

// 在TodoAction中定义的Action Creators
const TodoAction = {
  // 用一个函数包裹AppDispatcher.dispatch方法
  actionCreator
    create(todo) {
      AppDispatcher.dispatch({
        actionType: 'CREATE_TODO',
        todo
      });
    },
    ...
};
  • store和Dispatcher

store就是整个程序所需要的数据。store是单例的。现在来创建TodoStore,它存放了所有的文章列表。不同类型的数据应该创建多个store,假如程序里还存在用户信息,就应该再创建UserStore.js

// ./stores/TodoStore.js
// 单件类型的一个JavaScript Object
const TodoStore = {
  // 存放所有文章的列表,里面有两条默认的数据
  todos: [{ id: uuid.v4(), content: 'first one'}, { id: uuid.v4(), content: '2nd one '}],
  getAll() {
    return this.todos;
  },
  addTodo(todo) {
    this.props.push(todo);
  },
  deleteTodo(id) {
    this.todos = this.todos.filter(item => item.id !== id);
  }
};

Dispatcher的另外一个API方法就是register,它可以注册不同事件的处理回调,并且在回调中队store进行处理。

// ./stores/TodoStore.js
...
AppDispatcher.register((action) = > {
  switch(action.actionType) {
    case 'CREATE_TODO':
      TodoStore.addTodo(action.todo);
      break;
    case 'DELETE_TODO': 
      TodoStore.deleteTodo(action.id);
      break;
    default:
      // 默认操作
  }
});

每个action对应dispatch传过来的一个action,包含actionType和对应的数据。store是更新数据的唯一场所,这是Flux的重要概念。actoin和Dispatcher并不和数据打交道。

  • store和view

现在,store已经发生变化,是时候由它来通知view,让view展示新的数据了。我们借助Node.js标准库EventEmitter,让store加上事件订阅特性,就可以把store和view联系在一起了:

npm install events -save
// 使用Object.assign方法把EventEmitter.prototype挂载到TodoStore上
const TodoStore = Object.assign({}, EventEmitter.prototype, {
  ...
  emitChange() {
    this.emit('change');
  },
  addChangeListener(callback) {
    this.on('change', callback);
  },
  removeChangeListener(callback) {
    this.removeListener('change', callback);
  }
});

AppDispatcher.register((action) => {
  switch(action.actionType) {
    case 'CREATE_TODO':
      TodoStore.addTodo(action.todo);
      // TodoStore已经更改,发送一个广播
      TodoStore.emitChange();
      break;
    case 'DELETE_TODO': 
      TodoStore.deleteTodo(action.id);
      TodoStore.emitChange();
      break;
    default:
      // 默认操作
  }
});
export default TodoStore;

store的变化已经使用emit方法广播出去,那么view层现在要做的就是接收这个变化的信号,同时更新UI。首先要在组件刚初始化的时候监听store的change事件,这样在store触发这个事件的时候,就会触发回调。那么,我们回到Todo.jsx组件中,在它的生命周期函数中加上这些事件监听:

// Todo.jsx
class Todo extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      todos: TodoStore.getAll()
    };
    this.createTodo = this.createTodo.bind(this);
    this.deleteTodo = this.deleteTodo.bind(this);
    this.onChange = this.onChange.bind(this);
  }
  componentDidMount() {
    // 初始化的时候在store中注册这个事件
    TodoStore.addChangeListener(this.onChange);
  }
  componentWillUnmount() {
    // 组件卸载的时候记得要清除这个事件绑定
    TodoStore.removeChangeListener(this.onChange);
  }
  onChange() {
    // store改变后触发的回到,用setState来更新整个UI
    this.setState({
      todos: TodoStore.getAll()
    });
  }
  ...
}

到现在已经完成了Flux的整个流程。当用户在view上有一个交互时,Dispatcher广播(dispatch方法)一个action(就是一个Object对象,里面包含action的类型和要传递的数据),在整个程序的总调度台(Dispatcher)里注册了各种类型的action,在对应的类型中,store(也是一个Object对象,实现了订阅-发布的功能)对这个action进行响应,对数据做响应的处理,然后触发一个自定义事件,同时在view上注册这个store的事件回调,响应这个事件并重新渲染界面。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容