HuangHongRui / Notebook

:pencil2: Yeah.. This's My NoteBook...:closed_book:
0 stars 0 forks source link

React [6](Flux) #42

Open HuangHongRui opened 7 years ago

HuangHongRui commented 7 years ago

前面感受到完全用React来管理应用数据的麻烦,所以需要Redux这种管理应用的框架

Flux

了解Redux先需要从Flux说起,因为Redux是Flux思想的理你中实现方式。 通过了解Flux,可知道Flux和Redux框架贯彻的最重要观点——单向数据流 通过发现Flux的缺点来深刻认识Redux相对Flux 的改进之处!!

在MVC(model-view-controller)世界中,React相当于V只涉及页面的渲染,涉及应用的数据管理部分,交给M和C。 但Flux不是一个MVC框架,它认为MVC框架存在大问题,所以推翻它,并用新思维来管理数据流转。

【MVC的缺陷】

Model(模型)辅助管理数据,大部分业务逻辑也该放于此 View(视图)渲染用户界面,避免在View中涉及业务逻辑 Controller(控制器)接受用户输入,根据用户输入调用对应的Model部分逻辑,产生的数据结果交由View渲染出必要的输出

image

这样逻辑划分,实质上与一个应用划分为多个组件一样,就是’分而治之‘

Facebook一开始也使用此种框架,后面发现“MVC真的很快就变得非常复杂” 每当程序猿要增一新功能时,对代码的修改很容易引入新Bug,因为不同模块之间的依赖关系让系统变得脆弱而不可预测,对刚入团队的新手,更是举步艰难,因为不知道修改代码会造成什么后果,如保险会发现寸步难移,如放手干,可能引发很多Bug【Mvc不适合Facebook】

Facebook描述的MVC框架,我们可看到, Model和View之间缠绕着复杂的依赖关系,相互之间的调用,乱!

image

可能画风和通常描述的MVC框架不一样,这是因为MVC提的数据很理想!!但实际框架实现中,总是允许View和Model直接通信。(但直接对话就是灾难!!)

HuangHongRui commented 7 years ago

对于MVC框架,为了让数据流可控, Controller 应该是中心!!当View要传递消息给Model时,应该调用Controller,同样Model要更新View时,也应该通过Controller来引发新的渲染。。。

HuangHongRui commented 7 years ago

Flux的特点:更严格的数据流控制

Flex框架

image

Flux 分为4部分,

  1. Dispatcher 处理动作分发,维持Store之间的依赖关系
  2. Store 负责存储数据和处理数据相关逻辑
  3. Action 驱动Dispatcher的Js对象
  4. View 视图部分,负责显示用户界面

与MVC结构对比,Dispatcher 相当于 Controller ,Store 相当于 Model , View 相当于View,Action可理解为 用户请求

MVC中,系统提供的服务,通过Controller暴露函数来实现。每增一功能,Controller往往要增一函数。 Flux中,新增功能不需要Dispatcher新增函数,Dispatcher自始至终只需暴露一个函数——Dispatch,当需新增功能时,要做的是新增一种新的Action类型,Dispatcher的对外接口并不用改变。

当需扩充应用所能处理的‘请求’时,MVC方法需要增加的Controller,而Flux则只增加新的Action。

HuangHongRui commented 7 years ago

使用Flux 需要安装Flux npm install --save flux

  1. Dispatcher

首先创造一个Dispatcher,几乎所有应用都只需要拥有一个 Dispatcher,创造一个唯一 Dispatcher 对象。

import {Dispatcher} from 'flux'
export dafault new Dispatcher()

引入 flux 库中的 Dispatcher 类,创造一个新对象作为文件默认输出。 其他代码中,会引用这个全局唯一的Dispatcher对象。

Dispatcher 存在的作用,是用来派发action。

HuangHongRui commented 7 years ago
  1. action

代表一个‘动作’。一个普通的 Js 对象,该对象不自带方法,代表一个动作的纯数据。

作为管理,action对象必须有一个名为 type 的字段,来代表这个Action的类型,为了记录日志和debug方便,此type应该是字符串类型。

定义action 通常需要分为2文件,

分为2文件的主原因是在 Store 中会根据action类型做不同操作,所以有导读导入action类型的需要。

定义action 的类型:

export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';

例子中,用户只能做2个动作,点击 + 和 -, 所以只有两个action类型。

再在另一个文件里定义action构造函数:

import * as ActionTypes from './ActionTypes.js';
import AppDispatcher from './AppDispatcher.js';

export const increment = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.INCREMENT,
    counterCaption: counterCaption
  });
};

export const decrement = (counterCaption) => {
  AppDispatcher.dispatch({
    type: ActionTypes.DECREMENT,
    counterCaption: counterCaption
  });
};

code里面定义的 不是action对象本身,而是能够产生派发action对象的函数。 代码中引如 action类型 和 APPDispatcher 文件,是要直接使用Dispatcher 这个文件导出的两个action构造函数 increment 和 decrement,当这2函数被调用时, 创造了对象的action对象,并立即通过AppDispatcher函数派发出去。

HuangHongRui commented 7 years ago
  1. store

store 是一对象,用于存应用状态,同时接受Dispatcher派发的动作, 根据动作来决定是否要更新应用状态。

因使用了Flux后,代码文件数量增多,所以创建一个store子目录,来放置所有的Store代码。

当Store的状态发生变化时,需要通知应用的其他部分作必要的响应2.在应用中,做出响应的部分是View部分,但不应该硬编码这种联系,应该用消息的方式建立 Store 和 View 的联系。这就是为什么 扩展 EventEmitter.prototype , 等于让 CounterStore成了EventEmitter对象,一个EventEmitter实例对象支持下面相关函数:

对于CounterStore对象,enitChange/addChangeListener/removeChangeListerner 函数就是利用EventEmitter上面的三个函数完成对 CounterStore 状态更新的广播/添加坚挺函数和删除坚挺函数等操作。

CounterStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    counterValues[action.counterCaption] ++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.counterCaption] --;
    CounterStore.emitChange();
  }
});

这是最重要一个步骤, 要把CounterStore注册到全局唯一的Dispatcher上, Dispatcher有一个函数叫做register,接受一个回调函数作为参数。返回值是一个token。 这个token可以用于Store之间的同步,在CounterStore 中还用不上这个返回值,再SummaryStore文件中会用到。

register 接受的这个回调函数参数,当通过register函数把一个回调函数注册到Dispatcher之后, 所有派发给Dispatcher的action对象,都会传递到这个回调函数中来。。

例如 通过 Dispatcher派发的一个动作:

AppDispatcher.dispatch({
    type: ActionTypes.INCREMENT,
    counterCaption: ‘first’
  });

在CounterStore注册的回调函数就会被调用,唯一的一个参数就是那个action 对象,回调函数要做的,就是根据action对象来决定改如何更新自己的状态。

作为一个普遍接受的传统,action对象中必须要有一个type字段,类型是字符串,用于表示这个action 对象是什么类型,如上面派发的action 对象,type为”increment“,表示一个计数器的”+1“动作,如有必要,一个action对象还可以包含其他的字段,上面的Action对象中还有一个counterCaption 字段值为 ‘first’,标识名字为first的计数器 例子中action对象的type和counterCaption字段结合在一起,可以确定是哪个计数器应该做加1或减1的动作,上面例子动作含义:’名字为first的计数器要做加1动作‘

根据不同的type,会有不同的操作,所以注册回调函数很自然有一个模式!!就是函数体是一串 if-else条件语句或是 switch 语句,而条件语句的跳转条件都是针对参数action对象的type字段

CounterStore.dispatchToken = AppDispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    counterValues[action.counterCaption] ++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.counterCaption] --;
    CounterStore.emitChange();
  }
});

代码中,如果action.type是INCREMENT,就根据action对象字段counterCaption确定是哪个计数器,再吧counterValue上对应的字段做加1操作;同样如果type是DECREMENT就做对应减一。。

无论加一或减一,最后都要调用CounterStore.emitChange函数,加入有调用者通过`CounterStore.addChangeListener关注了counterStore的状态变化,这个emitChange函数调用就会引发监听函数的执行。。

SummaryStore.js

function computeSummary(counterValues) {
  let summary = 0;
  for (const key in counterValues) {
    if (counterValues.hasOwnProperty(key)) {
      summary += counterValues[key];
    }
  }
  return summary;
}

const SummaryStore = Object.assign({}, EventEmitter.prototype, {
  getSummary: function() {
    return computeSummary(CounterStore.getCounterValues());
  }

SummaryStore 并没有存储组件的状态,当getSummary 被调用,他是直接从CounterStore中获取状态计算的。 SummaryStore不存储数据,而是每次对getSummary 的调用,都实时读取CounterStore.getCounterValues, 然后实时计算出总和返回给调用者。。

可见虽名为Store,但并不表示一个Store必须要存储什么东西,store只是提供获取数据的方法,而store提供的数据,完全可以另一个Store计算的来。。

SummaryStore 在Dispatcher 上注册的回调函数也和CounterStore不大一样,

SummaryStore.dispatchToken = AppDispatcher.register((action) => {
  if ((action.type === ActionTypes.INCREMENT) ||
      (action.type === ActionTypes.DECREMENT)) {
    AppDispatcher.waitFor([CounterStore.dispatchToken]);

    SummaryStore.emitChange();
  }
});

SummaryStore 通过APPDispatcher 函数注册了一个回调函数, 用于接受派发的action对象。

一个action对象被派发给所有回调函数,这就产生一个问题!!按照什么顺序调用各个回调函数呢?

即使Flux按照register调用的顺序去调用各个回调函数,但也无法把握各个store哪个先装载从而调用register函数,所以可以认为Dispatcher调用回调函数的顺序完全是无法预期的,不可假设它会按照我们期望的顺序调用。。

设想下,当INCREMENT 类型的动作条派发了,如果首先调用 SummaryStore的回调函数,这个回调函数中立即用emitChange通知监听者,这时监听者会立即通过SummaryStore的getSummary获取结果,而这个getSummary是通过CounterStore暴露的getCounterValues函数获取当前计数器值,计算出总和返回~然后这时,INCREMENT动作还没来得及派发到CounterStore啊!!!!也就是说CounterStore的getCounterValue返回的还是一个未更新的值,那么SummaryStore的getSummary返回值也就是一个错误的值了

解决该问题需要靠 Dispatcher 的 waitFor 函数!!

在SummaryStore的回调函数中,之前在CounterStore中注册回调函数时,保存下来的dispatchToken终派上用场。 Dispatcher的waitFor可以接受一个数组作为参数。数组中每个元素都是一个DispatcherRegister函数的返回结果,也就是所谓的dispatchToken, 这个waitFor函数告诉Dispatcher,当前的处理必须要暂停,直到dispatchToken代表的那些已注册回调函数执行结束才能继续。

Js是单线程的语言,不可能有线程之间的等待这回事,这个waitFor函数并不是多线程实现的,只在调用waitFor时,把控制权交给Dispatcher,让Dispatcher检查dispatchToken代表的回调函数有没被执行,如已执行,那直接接续,如没执行,那就调用dispatchToken代表的回调函数之后waitFor才返回。。

回到上面例子,即使SummaryStore 比 CounterStore 提前接受到action对象,在emitChange中调用waitFor,也就能保证在emitChange函数被调用的时候,CounterStore也已处理过这个action对象,一起完美解决。。

需要注意的一个事实: Dispatcher 的register函数,只提供了注册一个回调函数的功能,但却不能让调用者在register时选择监听某些action, 换句话说,每个register的调用者只能请求:”当有任何动作被派发时调用我“,但不能请求:”当这种类型还有那种类型的动作被派发时,调用我“

**当一个动作被派发时,Dispatcher 就是简单地把所有注册的回调函数全都调用一遍,至于这个动作是不是对方关心的,Flux的Dispatcher不关心。。

HuangHongRui commented 7 years ago
  1. View

Flux框架下, View 并不是说必须要使用 React , View本身就是一个独立的部分!,可以用任何一种UI库来实现。。

除非有大量历史遗留代码需要利用UI,否则....

存在与Flux框架中的 React 组件需要实现下面几个功能:

  render() {
    return (
      <div style={style}>
        <Counter caption="First" />
        <Counter caption="Second" />
        <Counter caption="Third" />
        <hr/>
        <Summary />
      </div>
    );
  }

组件实例中只有caption属性。计数值包括初始值都放到CounterStore中了。 所以创建Counter组件实例时没必要制定initValue了

Counter组件中的state 应成为 Flux Store 上状态的一个同步镜像,为了保持两者一致,除了在构造函数中的初始化之外,在之后当CounterStore上状态变化时, Counter组件也要对应变化:

  shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.caption !== this.props.caption) ||
           (nextState.count !== this.state.count);
  }

  componentDidMount() {
    CounterStore.addChangeListener(this.onChange);
  }

  componentWillUnmount() {
    CounterStore.removeChangeListener(this.onChange);
  }

componentDidMount 函数中通过 CounterStore.addChangeListener 函数监听CounterStore的变化,只要CounterStore发生变化,Counter组件的onChange函数就会被调用。与componentDidMount 函数中监听事件相对应,在componentWillUnmount函数中删除了这个监听。

React组件 如何派发 action, 代码:

    Actions.increment(this.props.caption);
  }

  onClickDecrementButton() {
    Actions.decrement(this.props.caption);
  }

  render() {
    const {caption} = this.props;
    return (
      <div>
        <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button>
        <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button>
        <span>{caption} count: {this.state.count}</span>
      </div>
    );
  }

注意到,在Counter组件中有两处用到 CounterStore 的getCounterValues 函数的地方 第一次是在构造函数中初始化,第二处是在响应CounterStore状态变化的onChange函数。 同一个store状态,为了转换为React组件的状态,有两次重复调用,不是很好,但是React组件状态就是这样,在构造函数中要对this.state初始化,更新它就要调用setState函数。

HuangHongRui commented 7 years ago

Flux 的优势。。

Flux的实现版本,用户的操作引发的是一个 ‘动作’ 的派发,这个派发的动作会发送给所有的Store对象,引起Store对象的状态改变,而不是直接引发组件的状态改变。 因为组件的的状态是Store状态的映射,所以改变了Store对象也就触发了React组件对象的状态改变,从而引发了界面的重新渲染。。

Flux 的好处,最重要的就是 ”单向数据流“ 的管理方式

Flux 的理念: 如果要改变界面,那么改变Store中的状态,如果要改变Store 中的状态爱,必须派发一个action对象,这是规矩,此规矩下,想要追溯一个应用逻辑就非常容易

在Store中,只有get方法,无set方法,不可能直接修改其内部状态,View只能通过get方法获取Store状态,无法直接去修改状态,如果想修改Store状态,只有派发一个Action对象给Dispatcher

简单来说,Flux的体系下,驱动一个动作始于一个动作的派发。。

HuangHongRui commented 7 years ago

Flux的不足

  1. Store 之间的依赖。

如果两个store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数 虽然Flux这个涉及确实解决了Store之间的以来关系,但是,这样明显的模块之间的依赖,不太好,毕竟最好的以来管理是根本不让依赖产生。。

  1. 难以进行服务端渲染。

  2. Store 混杂逻辑和状态