如何在使用 Flux 时使 ReactJS 更好地发挥作用
我最近开始学习 ReactJS。有了 GitHub 上面这个很棒的文档让我觉得它很容易学习。 我用 ReactJS 创建了一个示例应用,而且它工作得很好!
有了一些经验之后,我想先提一下它的两个要点:
HTML 和 Javascript 在一个文件时会变得容易维护。
在组件驱动开发中,DOM 拆分成组件可以使其可重用以及易测试。
然后我听说了 React with Flux,我很想知道为什么当React自身很好的情况下我们还需要Flux(why do we need Flux when React is fine on its own)。 我在 ReactJS 方面没有太多的开发经验,这就是为什么我无法意识到Flux的作用。基于对学习的渴望,我告诉自己振作一点,学习Flux的同时也帮助培训他人。
用 ReactJS 创建一个应用,但最初并没有使用 Flux ,这样便于理解它的优势,比如:
- 可维护性
- 可读性
- 单向数据流
为了在实践中有一样的效果,我们来看看使用 Flux 和不使用 Flux 来创建应用程的区别。
让我们从了解 Flux 的基本定义/架构开始。
Flux 框架
Flux 有以下四个要点
- Action(操作):Actions 非常简单,因为它们只需要从 View 接收请求并将其传递给 Dispatcher 。 它充当的是View和Dispatcher之间的中介。
- Dispatcher(调度): Dispatcher 负责将信息传递给 Store。 它通过 Dispatcher 中的 <this.dispatch> 方法来完成工作。
- Store(存储): Stores 与数据一起工作。它根据每个来自 View 的请求与后端服务器通信。
- View(视图): Views 负责显示信息。如果视图需要信息就会从 Store 中获取,如果需要执行某些操作或者更新、添加某些信息则会告知 Action。Action 调用 Dispatcher ,Dispatcher 反过来从Store获取数据。
假设要创建一个 TODO 应用。这是一个单页应用,分为以下几个组件:
- Header
- Todo Count
- Todo Form
- Todo List
- Todo Item
在浏览器中,组件将如下图所示:
预期的特性
- Todo Count 显示所有 Todo Item 的数量。
- Todo Form 有一个输入框。
- 提交表单时,新的 Todo Item 会添加到 Todo List,Todo Count 也会相应地增加。
不使用 Flux 的应用
创建 TodoItem 类用于渲染单个的 todo item。 它将从父组件接收 todo item 的信息:
var TodoItem = React.createClass({ render: function() { return ( <li> { this.props.user } - { this.props.task} </li>) } })
下面的组件是 TodoList,它负责渲染所有的 todo items。这个组件通过 props 从父类获取“数据”。“this.props.data” 是我们之前创建的用于迭代和调用 TodoItem 的一个列表。
var TodoList = React.createClass({ render: function() { var TodoTasks = this.props.data.map(function(todoItem) { return ( < TodoItem user = { todoItem.user } task = {todoItem.task} key = { todoItem.id } /> ) }) return ( < div className = "todo list" > <ul> Todo List {TodoTasks} </ul> </div> ) } });
TodoCount 组件负责显示 items 的计数。 它从 TodoHeader 组件获取计数。
var TodoCount = React.createClass({ render: function() { return ( <div> { this.props.count } </div>) } });
TodoHeader 组件显示应用程序的头,它调用 TodoCount 组件显示所有 items 的计数。
var TodoHeader = React.createClass({ render: function() { return ( <nav> Header < TodoCount count = { this.props.count} /> </nav> ) } })
下面是 TodoForm 组件。 我们同样关注它是因为除了渲染之外它还要创建数据。
handleSubmit 方法在单击提交按钮时调用,它调用从 Application 组件获取的 Todo Submit 方法。
我当时有一些疑问:
- 为什么要用这个方法?
- 为什么 Application 组件提交表单而不是 TodoForm ?
原因是 Application 组件是所有组件的父/祖父组件。并且数据流是从父组件到子组件的并且子组件不能改变父组件的状态。此外,同级组件之间不能有任何通信数据流。
现在如果 TodoForm 要提交表单,那么这些信息要如何传递到 TodoList 或 TodoHeader 组件?
这就是为什么要让 Application 组件负责提交表单。
var TodoForm = React.createClass({ getInitialState: function() { console.log("inside todo form of initial") return { user: '', task: '' } }, handleUserChange: function(e) { this.setState({ user: e.target.value }) }, handleTaskChange: function(e) { this.setState({ task: e.target.value }) }, handleSubmit: function(e) { e.preventDefault(); this.props.onTodoSubmit({ user: this.state.user, task: this.state.task }) this.setState({ user: '', task: '' }) }, render: function() { return ( <form className = "todoform" onSubmit = { this.handleSubmit } > <input type = "text" placeholder = "your name" value = { this.state.user } onChange = { this.handleUserChange } /> <input type = "text" placeholder = "your task" value = { this.state.task } onChange = { this.handleTaskChange } /> <input type = "submit" value = "submit" /> </form> ) } })
应用组件是所有组件的父组件。于是它涉及的方法需要讨论一下。
loadDataFromServer – 它会从服务器加载数据,但在例子中没有任何服务器,所以就将数据硬编码了。
handleTodoSubmit – 这个方法将由 TodoForm 调用,就像 TodoForm 组件中讲解的那要。。
当这个方法被调用时,它将随着新创建的 Todo item 改变状态,这个新的 Todo item 会触发Application 组件的重新渲染,并且所有子组件将随着新的信息而更新。
var TODO = React.createClass({ getInitialState: function() { return { data: [] } }, loadDataFromServer: function() { this.setState({ data: [ { id: 1, user: "Adam", task: "This is task1"}, { id: 2, user: "Ricky",task: "This is task2"} ] }) }, componentDidMount: function() { this.loadDataFromServer() }, handleTodoSubmit: function(todo) { todo.id = Date.now() var todos = this.state.data var newTodos = todos.concat([todo]) this.setState({ data: newTodos }) }, render: function() { return ( <div className = "todo"> <TodoHeader count = { this.state.data.length } /> <TodoForm onTodoSubmit = { this.handleTodoSubmit } /> < TodoList data = { this.state.data } /> </div> ) } }) ReactDOM.render( < TODO /> , document.getElementById("example"))
可以看到,如果同级组件想要彼此通信,就需要将数据传递到它们的父组件。
像是例子中的 Todo Form 想告知 Todo List 有新的 item 被添加了。
这就是为什么 Application 组件传递回调方法 handleTodoSubmit。 所以在 Todo Form 提交 时它通过回调调用 Application 组件的 handleTodoSubmit 方法。 而 handleTodoSubmit 正在更新 Application 的状态,这使得通过更新 Todo Item 和 Todo Header 来重新渲染 Application 组件。
在例子中,只有1个层级,但在实时情况下,可能有多个层级,而最内层的子层想要更新其他层的最内层子层。 这就需要传递的回调方法覆盖所有层级。
但这会使其不易维护,也大大降低可读性,这也使 React 的优势大打折扣。
现在试试相同的应用使用 Flux
正如我们所看到的,Flux 有4层。 根据它的这个结构编写一个代码。
Action
Actions 被 Views 调用。如果 View 要在 Store 中更新数据,则 View 会告知 Action这个变更 。 这里创建了 AppAction。 在这里,只有一个action,即 addItem。
当要添加一个新的 todo Item 时,这将会被调用。 这个 action 进一步调用 dispatcher(AppDispatcher) 的handleViewAction。
var AppDispatcher = require('../dispatchers/app-dispatcher'); var AppAction = { addItem: function(item) { AppDispatcher.handleViewAction({ actionType: 'ADD_ITEM', item: item }) } }
Dispatcher
在 AppDispatcher 中,定义了由 AppLIcation 调用的 handleViewAction 。 在handleViewAction 中,action 会被传递用做告知执行什么 action 。
handleViewAction 里面有一个 this.dispatch ,这是一个 Dispatcher 的预定义方法。这个方法会在内部调用 Store 。
var Dispatcher = require("flux").Dispatcher; var assign = Object.assign; var AppDispatcher = assign(new Dispatcher(), { handleViewAction: function(action) { console.log('action', action) this.dispatch({ source: 'VIEW_ACTION', action: action }) } });
Store(存储)
接下来我们一个个地讨论 Store 的方法。
dispatcherIndex – 由于 AppDispatcher.register,执行从这里开始。每当调用Dispatcher 的 dispatch 方法时,它会在所有定义 AppDispatcher.register 的地方传递 action 的信息。
在例子中,只有一个 action —— “ADD_ITEM”,但在大多数情况下,将有多个 action 。 因此,需要首先定义 action 的类型,基于此,还会执行 actions 并调用 emit 方法。
这里,在 dispatcherIndex 方法中调用 addTodoItem 方法,之后是调用 emitChange。
addTodoItem – 在这个方法中,除了将新的 todo item 添加到到 todoItems 数组不会做其它事情。
emitChange – 在 emitChange 中,使用 this.emit(CHANGE_EVENT) 方法可以允许 CHANGE_EVENT 的监听者知道某些东西发生了变化。
addListener – Views 用这个方法来监听 CHANGE_EVENT。
removeListener – Views 用这个方法来移除监听器。
getTodoItems – 这个方法会返回所有的 todos。它会被 TodoList 组件调用。
getTodoCount – 这个方法会返回所有 todos 的计数。它会被 TodoCount 组件调用。component.
var AppDispatcher = require('../dispatchers/app-dispatcher'); var assign = Object.assign; var EventEmitter = require('events').EventEmitter; var CHANGE_EVENT = 'change'; var todoItems = [ { id: 1, user: "Adam", task: "This is task1"}, { id: 2, user: "Ricky", task: "This is task2"} ]; var AppStore = assign(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT) }, addListener: function(callback) { this.on(CHANGE_EVENT, callback) }, removeChangeListener: function(callback) { this.removeListener(CHANGE_EVENT, callback) }, dispatcherIndex: AppDispatcher.register(function(payload) { var action = payload.action; if (action.actionType == "ADD_ITEM") { this.addTodoItem(payload.action.item); } AppStore.emitChange(); return true; }), getTodoItems: function() { return todoItems; }, getTodoCount: function() { return todoItems.length; }, addTodoItem: function(todo) { todoItems.push(todo); } })
View(视图)/Components(组件)
在 TodoTask 中,与上面的例子一样没有改变任何东西。
var TodoTask = React.createClass({ render: function() { return ( <li> { this.props.user } - { this.props.task } </li> ) } })
在 TodoList 组件中,通过在 getInitialState 中调用AppStore.getTodoItems 直接从 Store 提取 todo items。
现在的问题是,“这个组件怎样才能知道新的 todo item 是什么时候添加的?”
答案 就在 componentWillMount 中。在这个方法中,调用 AppStore.addChangeListener ,它监听在 Store 的 addChangeListener 中定义的事件。如果有任何更改,那么它将调用 _onChange 来重置状态。
var TodoList = React.createClass({ getInitialState: function() { return { todoItems: AppStore.getTodoItems() } }, componentWillMount: function() { AppStore.addChangeListener(this._onChange) }, componentWillUnmount: function() { AppStore.removeChangeListener(this._onChange) }, _onChange: function() { this.setState({ todoItems: AppStore.getTodoItems() }) }, render: function() { var TodoTasks = this.state.todoItems.map(function(todoTask) { return ( < TodoTask user = { todoTask.user } task = { todoTask.task } key = { todoTask.id } /> ) }) return ( <div className = "todo list"> <ul> Todo List { TodoTasks } </ul> </div> ) } });
与 TodoList 类似,TodoCount 也是从 Store 获取数据,并且在 componentWillMount 中定义了一个监听器。
var TodoCount = React.createClass({ getInitialState: function() { return { count: AppStore.getTodoCount() } }, componentWillMount: function() { AppStore.addChangeListener(this._onChange) }, componentWillUnmount: function() { AppStore.removeChangeListener(this._onChange) }, _onChange: function() { this.setState({ count: AppStore.getTodoCount() }) }, render: function() { return ( < div > { this.state.count } < /div>) } });
TodoHeader 组件和前面的例子类似。
var TodoHeader = React.createClass({ render: function() { return ( <nav> Header <TodoCount /> </nav> ) } })
在 TodoForm 中,提交表单时, AppAction.addItem 会被调用,就像前面讨论的一样通过 Dispatcher 让 Store 知道。
var TodoForm = React.createClass({ getInitialState: function() { return { user: '', task: '' } }, handleUserChange: function(e) { this.setState({ user: e.target.value }) }, handleTaskChange: function(e) { this.setState({ task: e.target.value }) }, handleSubmit: function(e) { e.preventDefault(); AppAction.addItem({ user: this.state.user, task: this.state.task }) this.setState({ user: '', task: ''}) }, render: function() { return ( <form className = "todoform" onSubmit = { this.handleSubmit} > < input type = "text" placeholder = "your name" value = {this.state.user} onChange = { this.handleUserChange } /> < input type = "text" placeholder = "your task" value = {this.state.task} onChange = {this.handleTaskChange} /> < input type = "submit" value = "submit" / > </form> ) } })
现在 Application 组件的工作只是渲染3个组件:TodoHeader、TodoForm、TodoList。
var Application = React.createClass({ render: function() { return ( <div className = "todo" > < TodoHeader / > < TodoForm / > < TodoList / > </div> ) } }) ReactDOM.render( < Application / > , document.getElementById("example"))
结论
通过两个代码的比较,不难看出,创建应用的时候不使用 Flux 仍然很容易。但是如果你了解 Flux 的流程,那么你的选择永远是 Flux ,因为 Flux 的流程不会随应用变得复杂二复杂。 它为我们提供了可维护性,可读性,单向数据流这些额外的好处。
总而言之,重温一下流程。
在 TODO 组件中,渲染了3个组件<TodoHeader>、<TodoForm>、<TodoList>。 没必要传递任何回调,没必要定义任何状态,因为 <TODO> 组件除了调用其他组件之外不会做任何事。
TodoHeader 组件也是除了渲染或调用其他组件之外不会做任何事。
在 TodoCount 组件中,需要显示所有 items 的计数。可以从通过 TodoCount 渲染的 AppStore 获取计数。但是计数可能在渲染 TodoCount 组件之后更新。 这就是为什么在 componentWillMount 中添加了监听器,并在 componentWillUnmount 方法中移除。因此,如果在 Store 中出现任何与计数相关的更新,那么它会调用 _onChange 方法,并相应地更改计数的状态。
TodoList 组件显示 Todo Items 的列表,因此需要 TodoItems。在与 AppStore 通信方面,行为类似于 TodoCount 。
在 TodoForm 中,要提交表单并告知其他组件已经添加了新的item。所以在提交时,它只是传递信息到 AppAction 而不是告知其它组件,AppAction 会在AppDispatcher 的帮助下将其调度到 store 。而 AppStore 会更新/添加数据。