前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从flux到redux

从flux到redux

作者头像
一粒小麦
发布2019-12-05 21:28:46
8110
发布2019-12-05 21:28:46
举报
文章被收录于专栏:一Li小麦一Li小麦

从flux到redux

flux既是一个前端架构,更是一种状态管理的思想。

2013年,Facebook公司让React亮相的同时,也推出了Flux框架,React和Flux相辅相成,Facebook认为两者结合在一起才能构建大型的JavaScript应用。

MVC的缺陷

谈起MVC,我们想到的传统MVC架构可能是这样的:

但是,Facebook的工程部门在应用MVC架构时逐渐发现,对于非常巨大的代码库和庞大的组织,“MVC真的很快就变得非常复杂”。每当工程师想要增加一个新的功能时,对代码的修改很容易引入新的bug,因为不同模块之间的依赖关系让系统变得“脆弱而且不可预测”。对于刚刚加入团队的新手,更是举步维艰。

Facebook工程师眼中MVC的画风

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

而浏览器端MVC框架,存在用户的交互处理,界面渲染出来之后,Model和View依然存在于浏览器中,这时候就会诱惑开发者为了简便,让现存的Model和View直接对话。

所以Facebook提出了他们眼中的解决方案,那就是flux。

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

如果非要把Flux和MVC做一个结构对比,那么,Flux的Dispatcher相当于MVC的Controller,Flux的Store相当于MVC的Model,Flux的View当然就对应MVC的View了,至于多出来的这个Action,可以理解为对应给MVC框架的用户请求。

在MVC框架中,系统能够提供什么样的服务,通过Controller暴露函数来实现。每增加一个功能,Controller往往就要增加一个函数;在Flux的世界里,新增加功能并不需要Dispatcher增加新的函数,实际上,Dispatcher自始至终只需要暴露一个函数Dispatch,当需要增加新的功能时,要做的是增加一种新的Action类型,Dispatcher的对外接口并不用改变。当需要扩充应用所能处理的“请求”时,MVC方法就需要增加新的Controller,而对于Flux则只是增加新的Action

在react中使用flux

现在用flux重构上篇文章创造的计数器。

代码语言:javascript
复制
npm i flux -S

页面有三个参与计算的clickCounter组件,大致的思路是:每个组件都根据label获取对应的状态。

Dispatch

在src下创建Dispatcher:

代码语言:javascript
复制
// src/Dispatcher/index.js
import {Dispatcher} from 'flux';
export default new Dispatcher();

在这里,我们引入了dispatch类,然后创造一个新的对象作为这个文件的默认输出就足够了。在其他代码中,将会引用这个全局唯一的Dispatcher对象。

Dispatcher存在的作用,就是用来派发action,接下来我们就来定义应用中涉及的action。

Action

action顾名思义代表一个“动作”,不过这个动作只是一个普通的JavaScript对象,代表一个动作的纯数据,类似于DOM API中的事件(event)。甚至,和事件相比,action其实还是更加纯粹的数据对象,因为事件往往还包含一些方法,比如点击事件就有preventDefault方法,但是action对象不自带方法,就是纯粹的数据。

作为管理,action对象必须有一个名为type的字段,代表这个action对象的类型,为了记录日志和debug方便,这个type应该是字符串类型。定义action通常需要两个文件:

•第一个文件(src/Action/ActionTypes.js)定义action的类型:type:

代码语言:javascript
复制
export default {
    INCREMENT:'increment',
    DECREMENT:'decrement'
}

•第二个文件(src/Action/index.js)放构造函数,又名ActionCreator,它通过ActionTypes对象向store提交"事件"请求,这些事件名就是actionTypes内定义的名称:

代码语言:javascript
复制
import ActionTypes from './ActionTypes';
import Dispatcher from '../Dispatcher'

const increment=(label)=>{
    Dispatcher.dispatch({
        type:ActionTypes.INCREMENT,
        label
    })
}

const decrement=(label)=>{
    Dispatcher.dispatch({
        type:ActionTypes.DECREMENT,
        label
    })
}

export default {
    increment,
    decrement
}

这个index.js导出了两个action构造函数increment和decrement,当这两个函数被调用的时候,创造了对应的action对象,并立即通过AppDispatcher.dispatch函数派发出去。

store

一个Store也是一个对象,这个对象存储应用状态,同时还要接受Dispatcher派发的动作,根据动作来决定是否要更新应用状态。同时,它还可以被用来获取状态,并不一定非要储存状态。

counterStore

首先关注子组件计数器的状态,在src/store/CounterStore.js新建:

代码语言:javascript
复制
import Dispatcher from '../Dispatcher';
import ActionTypes from '../Action/ActionTypes';
import { EventEmitter } from 'events';

const CHANGE_EVENT = 'changed';

// 初始状态
const counterValues = {
  firstCount: 0,
  secoundCount: 0,
  thirdCount: 0
}

const CounterStore = Object.assign({}, EventEmitter.prototype, {
  // 获取整个状态
  getCounterValues: function () {
    return counterValues;
  },
  // 触发事件
  emitChange: function () {
    this.emit(CHANGE_EVENT);
  },
  addChangeListener: function (callback) {
    this.on(CHANGE_EVENT, callback)
  },
  removeChangeListener: function (callback) {
    this.removeListener(callback)
  }
});

CounterStore.dispatchToken = Dispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    // 更新引用对象,再触发changed事件
    counterValues[action.label]++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.label]--;
    CounterStore.emitChange();
  }
});

export default CounterStore;

上述的代码中,我们让CounterStore扩展了EventEmitter.prototype,等于让CounterStore成了EventEmitter对象,一个EventEmitter实例对象支持下列相关函数。

•emit函数,可以广播一个特定事件,第一个参数是字符串类型的事件名称;•on函数,可以增加一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数;•removeListener函数,和on函数做的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数,和on函数一样,第一个参数是事件名称,第二个参数是处理函数。要注意,如果要调用removeListener函数,就一定要保留对处理函数的引用。

对于CounterStore对象,emitChange、addChangeListener和removeChangeListener函数就是利用EventEmitter上述的三个函数完成对CounterStore状态更新的广播、添加监听函数和删除监听函数等操作。

注册到dispatcher

目前实现的Store只有注册到Dispatcher实例上才能生效。Dispatcher有一个函数叫做register,接受一个回调函数作为参数。返回值是一个token,这个token可以用于Store之间的同步。

代码语言:javascript
复制
// 注册的回调函数包含了业务方法
CounterStore.dispatchToken = Dispatcher.register((action) => {
  if (action.type === ActionTypes.INCREMENT) {
    counterValues[action.counterCaption] ++;
    CounterStore.emitChange();
  } else if (action.type === ActionTypes.DECREMENT) {
    counterValues[action.counterCaption] --;
    CounterStore.emitChange();
  }
});

现在我们来仔细看看register接受的这个回调函数参数,这是Flux流程中最核心的部分,当通过register函数把一个回调函数注册到Dispatcher之后,所有派发给Dispatcher的给Dispatcher的action对象,都会传递到这个回调函数中来。

总和计数器(SummaryStore)

总和计数器和counterStore的内容差不多,不同的地方在于就是提供了getSummary方法,此外,需要等待(waiteFor)CounterStore计算完毕后,才计算总和。

代码语言:javascript
复制
import Dispatcher from '../Dispatcher';
import ActionTypes from '../Action/ActionTypes';
import CounterStore from './CounterStore.js';
import { EventEmitter } from 'events';

const CHANGE_EVENT = 'changed';

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());
    },

    emitChange: function () {
        this.emit(CHANGE_EVENT);
    },

    addChangeListener: function (callback) {
        this.on(CHANGE_EVENT, callback);
    },

    removeChangeListener: function (callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }

});


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

export default SummaryStore;

可能你会好奇,目前CounterStore已经把所有的状态都储存了,为什么还会有一个SummaryStore?

的确,SummaryStore并没有存储自己的状态,当getSummary被调用时,它是直接从CounterStore里获取状态计算的。CounterStore提供了getCounterValues函数让其他模块能够获得所有计数器的值,SummaryStore也提供了getSummary让其他模块可以获得所有计数器当前值的总和。不过,既然总可以通过CounterStore.getCounterValues函数获取最新鲜的数据,SummaryStore似乎也就没有必要把计数器当前值总和存储到某个变量里。

事实上,可以看到SummaryStore并不像CounterStore一样用一个变量counterValues存储数据,SummaryStore不存储数据,而是每次对getSummary的调用,都实时读取CounterStore.getCounterValues,然后实时计算出总和返回给调用者。可见,虽然名为Store,但并不表示一个Store必须要存储什么东西,Store只是提供获取数据的方法,而Store提供的数据完全可以另一个Store计算得来

总和计数器关注点在于注册方法用了一个waitFor:

代码语言:javascript
复制
SummaryStore.dispatchToken = AppDispatcher.register((action) => {
  if ((action.type === ActionTypes.INCREMENT) ||
      (action.type === ActionTypes.DECREMENT)) {
    AppDispatcher.waitFor([CounterStore.dispatchToken]);
    SummaryStore.emitChange();
  }
});

这样我们已经注册了两个store到Disparcher上,派发事件的回调函数的顺序是怎样的呢?

可以认为Dispatcher调用回调函数的顺序完全是无法预期的,不要假设它会按照我们期望的顺序逐个调用。

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

在源码实现上:当一个派发动作发生后,Dispatcher会检查weitFor中的状态回调函数是否被执行了,只有被执行了,才会根据新的状态来计算。因此,即使SummaryStore比CounterStore提前接收到了action对象,在emitChange中调用waitFor,也就能够保证在emitChange函数被调用的时候,CounterStore也已经处理过这个action对象,一切完美解决。

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

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

view

view并不是非得使用react,你可以使用任何喜欢的哪怕是自创的页面框架。但无论是哪个架构,都少不了以下流程:

•创建时要读取Store上状态来初始化组件内部状态;•当Store上状态发生变化时,组件要立刻同步更新内部状态保持一致;•View如果要改变Store状态,必须而且只能派发action。

回到总体的页面,我们不需要再把子组件的计算逻辑放到最上层来了。

代码语言:javascript
复制
import React, { Component } from 'react';
import ClickCounter from './ClickCounter'
import AllCount from './AllCount';
const styles={
  app:{
    width:250,
    margin:'100px auto'
  }
}

class App extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <div style={styles.app}>
        <ClickCounter 
        label={'firstCount'} />
        <ClickCounter 
        label={'secoundCount'}/>
        <ClickCounter 
        label={'thirdCount'} />
        <hr />
        <AllCount />
      </div>
    );
  }
}

export default App;

clickCounter组件,需要在不同生命周期更新:

代码语言:javascript
复制
import React, { Component } from 'react'
import CounterStore from './stores/CounterStore';
import Actions from './Action'

const styles = {
    counter: {
        width: 250,
        display: 'flex'
    },
    showbox: {
        width: 80,
        textAlign: 'center'
    },
    label: {
        width: 120,
    }
}


class ClickCounter extends Component {
    constructor(props) {
        super(props)
          根据label从总体状态中拿到属于自己的`FIRST/SECOUND/THIRD`count
        this.state = {
            count: CounterStore.getCounterValues()[props.label]
        }
    }


    componentDidMount() {
          // 开启监听
        CounterStore.addChangeListener(this.onChange);
    }

    componentWillUnmount() {
        // 防止内存泄漏
        CounterStore.removeChangeListener(this.onChange);
    }

    onChange = () => {
        const newCount = CounterStore.getCounterValues()[this.props.label];
        this.setState({ count: newCount });
    }

    onClickIncrementButton = () => {
        Actions.increment(this.props.label);
    }

    onClickDecrementButton = () => {
        Actions.decrement(this.props.label);
    }

    render() {

        return (
            <div style={styles.counter}>
                <div style={styles.label}>{this.props.label}</div>
                <button onClick={this.onClickDecrementButton}>-</button>
                <div style={styles.showbox}>{this.state.count}</div>
                <button onClick={this.onClickIncrementButton}>+</button>
            </div>
        )
    }
}

export default ClickCounter;

在总和计数器上,就比较简单了:

代码语言:javascript
复制
import React, { Component } from 'react'
import SummaryStore from './stores/SummaryStore';

export default class extends Component {
    constructor(props) {
        super(props);
        this.state = {
            sum: SummaryStore.getSummary()
        }
    }

    componentDidMount() {
        // 监听变化
        SummaryStore.addChangeListener(this.onUpdate);
    }

    componentWillUnmount() {
        SummaryStore.removeChangeListener(this.onUpdate);
    }

    onUpdate = () => {
        this.setState({
            sum: SummaryStore.getSummary()
        })
    }

    render() {
        return (
            <div>Total Count: {this.state.sum}</div>
        );
    }
}

flux的优与劣

和纯react的state维护相比,flux架构下的计数器有了明显变化。

回顾一下纯React实现的版本,应用的状态数据只存在于React组件之中,每个组件都要维护驱动自己渲染的状态数据,单个组件的状态还好维护,但是如果多个组件之间的状态有关联,那就麻烦了。

比如ClickCounter组件和AllCount组件,AllCount组件需要维护所有ClickCounter组件计数值的总和,ClickCounter组件和AllCount分别维护自己的状态,如何同步AllCount和ClickCounter状态就成了问题,React只提供了props方法让组件之间通信,组件之间关系稍微复杂一点,这种方式就显得非常笨拙。

Flux的架构下,应用的状态被放在了Store中,React组件只是扮演View的作用,被动根据Store的状态来渲染。在上面的例子中,React组件依然有自己的状态。但是已经沦落为store的一个映射,而不是主动变化的数据。

因此flux的优势可以归结为"单向数据流"。

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

MVC最大的问题就是无法禁绝View和Model之间的直接对话,对应于MVC中View就是Flux中的View,对应于MVC中的Model的就是Flux中的Store,在Flux中,Store只有get方法,没有set方法,根本不可能直接去修改其内部状态,View只能通过get方法获取Store的状态,无法直接去修改状态,如果View想要修改Store状态的话,只有派发一个action对象给Dispatcher。

flux也存在一些缺点:

在Flux的体系中,如果两个Store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数。要通过waitFor函数告诉Dispatcher,先让CounterStore处理这些action对象,只有CounterStore搞定之后SummaryStore才继续。那么,SummaryStore如何标识CounterStore呢?靠的是register函数的返回值dispatchToken,而dispatchToken的产生,当然是CounterStore控制的,换句话说,要这样设计:

•CounterStore必须要把注册回调函数时产生的dispatchToken公之于众;•SummaryStore必须要在代码里建立对CounterStore的dispatchToken的依赖。

此外flux也难以在服务端进行渲染。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从flux到redux
    • MVC的缺陷
      • 在react中使用flux
        • Dispatch
        • Action
        • store
        • counterStore
        • 注册到dispatcher
        • 总和计数器(SummaryStore)
        • view
      • flux的优与劣
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档