在正式开始介绍 Flux 架构模式之前,我们首先会了解 React 独立架构的形态与问题,毕竟 Flux 架构模式的诞生也是为了解决这样一类问题。这一小节也算是抛转引出下面的 Flux 架构模式内容吧!

React 独立架构

对于「React 独立架构」这个名词,可能很多读者都感到很陌生。

这里首先介绍一下 ReactReact 是 Facebook 在 2013 开源的专注于视图层的 JavaScript 库。React 可以将用户界面很细化的抽象成许多组件,然后开发者可以很自由的将这些组件进行组合,最终形成功能丰富并可交互的页面。现在有很多框架实现了 MVCMVVM 的设计模式,而 React 不同,它似乎只钟爱于提供清晰、简洁的 View (视图)层。但是和模板引擎又不一样,React 封装了 ViewController。这样说来,利用 React 在不借助其他库的情况下实现应用也是可行的。

架构案例

为了更好地提现 React 独立架构的可行性和暴露出 React 独立架构的问题,这里特地展示了一个简单的案例。

Flux 系列一:React 独立架构 - 图1

案例展示的逻辑是:在页面上任意点击一个 Counter 组件的 “+” 或 “-” 按钮,所属 Counter 组件的计数值会发生相应的变化,同时在 Total Count 一栏会显示三个 Counter 组件当前展示的计数之和。

说到这里,大家或许已经想好了组件的拆分和每个组件业务逻辑的实现,脑海中的蓝图已经浮现了出来。但是在这里还是要对实现功能的关键逻辑进行梳理一下。

首先我们封装了 Counter 组件,实现了单个的计数器功能。在 Counter 组件中,每当点击 “+” 或 “-” 按钮时,都会触发当前组件内 state 状态属性 num 值的变化,代码如:

  1. // /Counter/index.js
  2. import React, { Component } from 'react';
  3. import './index.css';
  4. export default class Counter extends Component {
  5. constructor(props) {
  6. super(props);
  7. this.state = {
  8. num: 0
  9. }
  10. }
  11. // 优化组件重渲染
  12. shouldComponentUpdate(nextProps, nextState) {
  13. return nextState.num !== this.state.num;
  14. }
  15. // 1️⃣ 定义计数加
  16. handleIncrement = () => {
  17. this.handleUpdateNum('increment');
  18. }
  19. // 2️⃣ 定义计数减
  20. handleDecrement = () => {
  21. this.handleUpdateNum('decrement');
  22. }
  23. // 3️⃣ 处理组件内 state 中属性 num 值变化并通知父组件更新 total 总数值
  24. handleUpdateNum = updateType => {
  25. let { num } = this.state;
  26. switch(updateType) {
  27. case 'increment':
  28. this.setState({
  29. num: ++num
  30. });
  31. this.props.updateCount(updateType);
  32. return;
  33. case 'decrement':
  34. this.setState({
  35. num: --num
  36. });
  37. this.props.updateCount(updateType);
  38. return;
  39. default:
  40. return;
  41. }
  42. }
  43. render() {
  44. const { name } = this.props;
  45. const { num } = this.state;
  46. return (
  47. <div className="wrapper">
  48. <button onClick={this.handleIncrement}>+</button>
  49. <button onClick={this.handleDecrement}>-</button>
  50. <div>{name}: {num}</div>
  51. </div>
  52. )
  53. }
  54. }

在上面代码中,标有 1️⃣2️⃣3️⃣记号的方法是主要的逻辑。当用户在页面上点击 “+” 或 “-” 按钮时,会执行 handleIncrement 或 handleDecrement,这两个方法又会去执行统一的计数加减处理的 handleUpdateNum 方法。在 handleUpdateNum 方法中,会根据调用方法传进来的标识执行计数加减操作或者直接返回。计数的加减操作同时会更新本组件内的 state 属性 num 的值,随后执行由父组件传进来更新父组件本身 state 属性 count 总数的方法,以便更新总数的变化。

ControlPanel 组件的代码逻辑如下:

  1. // /ControlPanel.js
  2. import React, { Component } from 'react';
  3. import Counter from './components/Counter';
  4. import './ControlPanel.css';
  5. export default class ControlPanel extends Component {
  6. state = {
  7. count: 0
  8. }
  9. // 处理 count 总数的变化
  10. handleCount = updateType => {
  11. const { count } = this.state;
  12. switch(updateType) {
  13. case 'increment':
  14. this.setState({
  15. count: count + 1
  16. });
  17. return;
  18. case 'decrement':
  19. this.setState({
  20. count: count - 1
  21. });
  22. return;
  23. default:
  24. return;
  25. }
  26. }
  27. render() {
  28. return (
  29. <div className="control-wrapper">
  30. <Counter name="First count" updateCount={this.handleCount} />
  31. <Counter name="Second count" updateCount={this.handleCount} />
  32. <Counter name="Third count" updateCount={this.handleCount} />
  33. <div className="total">Total Count: {this.state.count}</div>
  34. </div>
  35. )
  36. }
  37. }

在 ControlPanel 组件中展示了三个不同的 Counter 组件,并向每个 Counter 组件中传入了 name 和 updateCount 方法属性进行了父子组件状态的同步更新。

例子说到这里就结束了。在没有借助任何第三方库的情况下实现了一个简单的计数器功能。虽然功能实现了而且其中的逻辑并没有让我们感到不适,但是其中隐藏着一些问题是我们必须要考虑的,特别当运用在实际的项目开发中时。

问题分析

Flux 系列一:React 独立架构 - 图2

在示例中,组件的状态变化流图如上图所示,当 Counter 子组件状态变化后,要马上通知 ControlPanel 父组件更新其内部的状态,然后在页面上展示最新的 count 总数值。这样一来,影响 ControlPanel 父组件状态的因素不仅包含自身内部的状态变化,另外还有 Counter 子组件状态的变化。那么问题就来了。如果由于某个 Counter 子组件在响应用户的加减操作时,没有及时通知到 ControlPanel 父组件更新状态,那么在页面上显示的各个计数器的和和计数总和就会存在不一致的情况,在这个时候根本无法知道是 Counter 子组件的问题还是 ControlPanel 父组件的问题。

如果是仔细分析的话,问题的可能性会有很多,可能是 Counter 子组件更新状态错误,或者是 Counter 子组件状态更新了但是没有通知到 ControlPanel 父组件更新状态,更或者根本就是 Counter 子组件更新状态正确且通知了ControlPanel 父组件更新状态,但是 ControlPanel 父组件实际上没有更新状态。

分析到这里,可能会有同学提出用将状态提取到最小公共父组件的方法来解决这个问题。实际的操作是将 Counter 子组件的状态提取到 ControlPanel 父组件,由 ControlPanel 父组件统一来管理。然后在用户点击 “+” 或 “-” 按钮来更新状态时,依然由 Counter 子组件调用 ControlPanel 父组件传递的方法。逻辑代码如下:

  1. // /Counter/index.js
  2. import React, { Component } from 'react';
  3. import './index.css';
  4. export default class Counter extends Component {
  5. // 优化组件重渲染
  6. shouldComponentUpdate(nextProps) {
  7. return nextProps.num !== this.props.num;
  8. }
  9. // 1️⃣ 定义计数加
  10. handleIncrement = () => {
  11. const { updateCount, name } = this.props;
  12. updateCount(name, 'increment');
  13. }
  14. // 2️⃣ 定义计数减
  15. handleDecrement = () => {
  16. const { updateCount, name } = this.props;
  17. updateCount(name, 'decrement');
  18. }
  19. render() {
  20. const { name, num } = this.props;
  21. return (
  22. <div className="wrapper">
  23. <button onClick={this.handleIncrement}>+</button>
  24. <button onClick={this.handleDecrement}>-</button>
  25. <div>{name}: {num}</div>
  26. </div>
  27. )
  28. }
  29. }
  1. // /ControlPanel.js
  2. import React, { Component } from 'react';
  3. import Counter from './components/Counter';
  4. import './ControlPanel.css';
  5. export default class ControlPanel extends Component {
  6. state = {
  7. count: 0,
  8. counterState: {
  9. FirstCount: 0,
  10. SecondCount: 0,
  11. ThirdCount: 0
  12. }
  13. }
  14. // 处理更新每个 counter 计数器状态和 count 总数的变化
  15. handleCount = ({ name, updateType }) => {
  16. const { count, counterState } = this.state;
  17. switch(updateType) {
  18. case 'increment':
  19. this.setState({
  20. count: count + 1,
  21. counterState: {
  22. ...counterState,
  23. [name]: counterState[name] + 1
  24. }
  25. });
  26. return;
  27. case 'decrement':
  28. this.setState({
  29. count: count - 1,
  30. counterState: {
  31. ...counterState,
  32. [name]: counterState[name] - 1
  33. }
  34. });
  35. return;
  36. default:
  37. return;
  38. }
  39. }
  40. render() {
  41. const { counterState } = this.state
  42. return (
  43. <div className="control-wrapper">
  44. <Counter name="FirstCount" num={counterState.FirstCount} updateCount={this.handleCount} />
  45. <Counter name="SecondCount" num={counterState.SecondCount} updateCount={this.handleCount} />
  46. <Counter name="ThirdCount" num={counterState.ThirdCount} updateCount={this.handleCount} />
  47. <div className="total">Total Count: {this.state.count}</div>
  48. </div>
  49. )
  50. }
  51. }

虽然所有的状态都提到 ControlPanel 父组件中进行管理,但是实际上状态的的改变流程还是和之前是一样的,也是当用户点击 “+” 或 “-” 按钮时,直接调用 ControlPanel 父组件传递过来更改计数器状态和 count 总数的方法,只是 Counter 子组件不用管理自身的状态了,但是 ControlPanel 父组件管理的状态变多了。很明显上面描述的问题依然存在。

在实际的项目开发中,组件之间的关系会更加的错综复杂,组件的状态管理和组件之间的状态传递成为了开发大型项之前必须解决的问题。
Flux 系列一:React 独立架构 - 图3

总结

React 独立架构引发的问题是明显的,组件间状态的复杂关系网,组件状态的大量冗余管理都是利用 React 开发大型应用的阻碍。为了解决这些我们不得不引入统一的状态管理,将状态管理的事物抛给第三方状态管理工具,让 React 更好的关注视图(View)。为此 Facebook 提出了著名的 Flux 状态管理模式。