第三章 从Flux到Redux(Flux篇)

        在前一章中我们已经感受到完全用React来管理应用数据的麻烦,在这一章中,我们将介绍Redux这种管理应用状态的框架,包含以下的内部:

        单项数据流框架的始祖Flux

        Flux理念的一个更强实现Redux;

        结合React和Redux。


3.1  Flux

        要了解Redux,首先要从Flux说起,可以认为Redux是Flux思想的另一种实现方式,通过了解Flux,可以认为Redux是Flux思想的另一种实现方式,通过了解Flux,我们可以知道Flux一族框架(其中包括(Redux)贯彻的最重要的观点--单向数据流,更重要的事,我们可以发现Flux框架的缺点,从而深刻地认识到Redux相对Flux的改进之处。

让我们来看看Flux的历史,实际上,Flux是和React同时面世的,在2013年,Facebook公司让React亮相的同时,也推出了Flux框架,React和Flux相辅相成,Facebook认为两者结合起来才能构建大型的JavaScript应用。

做一个容易理解的对比,React是用来替换jQuery的,那么Flux就是以替换Backbone.js、Ember.js等MVC一族框架为目的。

在MVC的世界里,React相当于V(也就是View)的部分,只涉及页面的渲染,一旦涉及应用的数据管理部分,还是交给Model和Controller,不过,Flux并不是一个MVC框架,事实上,Flux认为MVC框架存在很大问题,它推翻了MVC框架,并用一个新的思维来管理数据流转。

3.1.1 MVC框架的缺陷

MVC框架是业界广泛接受的一种前端应用框架类型,这种框架把应用分为三个部分:

Model(模型)负责管理数据,大部分业务逻辑也应该放在Model中;

View(视图)负责渲染用户界面,应该避免在View中涉及业务逻辑;

Controller(控制器)负责接受用户输入,根据用户输入调用对应的Model部分逻辑,把产生的数据结果交给View部分,让View渲染出必要的输出。

这样的逻辑划分,实质上把以一个应用划分为多个组件一样,就是“分而治之”。毫无疑问,相比把业务逻辑和界面渲染混在一起,MVC框架要先进的多。这种方式得到了广发认可,Facebook最初也是用这种框架。

但是,Facebook的工程部门逐渐发现,对于巨大的代码库和庞大的组织,MVC很快变的非常复实际的框架实现中,总是允许View和Model可以直接通信,而在MVC中直接让其对话是一种灾难。

有这么一个有意思的现象:凡是只在服务器端使用过MVC框架的朋友,就很容易理解和接受Flux。而对于已有很多浏览器端MVC框架经验的朋友,理解MVC和Flux要费点劲。

这个原因就是:服务器端MVC往往就是每个请求就只在Controller-Model-View三者之间走一圈,结果就返回给浏览器去渲染或者其他处理了。然后这个生命周期的Controller-Model-View就可以回收销毁了,这是一个严格意义的单向数据流;对于浏览器MVC框架,存在用户的交互处理,界面渲染出来后,Model和View依然存在于浏览器中,这时候就会诱发开发者为了简便,让现存的Model和View直接对话。

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

当Facebook推出Flux时,招致了很多质疑。很多人说,Flux不过是对数据流管理更加严格的MVVC框架而已,这种说明不准确,但一定意义说出了Flux的一个特点:更严格的数据流控制。

一个Flux应该包含四个部分,我们先粗略了解一下:

①Dispatcher,处理动作分发,维持Store之间的依赖关系。

②Store,负责存储数据处理数据相关逻辑。

③Action,驱动Dispatcher的JavaScript对象。

④View,视图部分,负责显示用户界面。

若非要将MVC与Flux做个结构对比:那么,Flux的Dispatcher相当于MVC的Controller,Flux的Store相当于MVC的Model,Flux的View对应MVC的View,至于多出来的Action,可以理解为对应给MVC框架的用户请求。

在MVC框架中,系统能够提供什么样的服务,通过Controller暴露函数来实现。每增加一个功能,Controller就要增加一个函数;在Flux的世界里,新增加功能并不需要Dispatcher增加新的函数,实际上,Dispatcher自始至终只需要暴露一个函数Dispatch,当需要增加新的功能时,要做的事增加一种新的Action类型,Dispatcher的对外接口并不用改变。

我们已基本了解Flux是怎么一回事,那么接下来实践一下看看怎么用Flux改进一下我们的React应用。

3.1.2 Flux应用

因为Redux其实与Flux一脉相承,从Flux例子入手,在理解Redux的时候就会感觉非常顺畅了。

为了理解Flux,首先通过命令行在项目目录下安装Flux。

npm install --save flux

利用Flux来实现ControlPanel应用的相关代码在-https://giyhub.com/mocheng/react-and-redux/tree/master/chapter-03/flux-上可以找到。最终页面应用效果和第二章完全 一样,通过不同实现方式,来体会每个方式的优劣。

1.Dispatcher

首先,创造一个Dispatcher,几乎所有应用都只需要拥有一个Dispatcher,本例也不例外。在src/AppDispatcher.js中,我们创建这个唯一Dispatcher对象:

import  {Dispatcher } from 'flux';

export default new Dispatcher();

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

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

2.action

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

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

定义action通常需要两个文件,一个定义action类型,一个定义action的构造函数(也称为action creator)。分成两个文件的主要原因是在Store中会根据 action类型做不同操作,也就有单独导入action类型的需要。

在src/ActionTypes.js中,我们定义action的类型:

export  const INCREMENT ='increment';

export  const DECREMENT ='decrement';

在这个例子中,用户只能做两个动作,一个是点击“+”按钮,一个是点击"-"按钮,所以我们只有两个action类型INCREMENT和DECREMENT。

现在我们在src/Actions.js文件中定义action构造函数:

import * as ActionTypes from './ActionTypes';

import AppDispatcher from './AppDispatcher.js';

export const increment = (counterCaption)=>{

AppDispatcher.dispatcher({

type:ActionTypes.INCREMENT,

counterCaption:counterCaption

});

};

export const decrement = (counterCaption)=>{

AppDispatcher.dispatcher({

type:ActionTypes.DECREMENT,

counterCaption:counterCaption

});

};

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

派发出去的action对象最后怎么样了呢?在下面关于Store的部分可以看到。

3.Store

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

接下来创造Store相关代码,便于代码维护,我们在src下创建一个子目录stores,在这个目录放置所有Store代码。

在前面章节的ControlPanel应用例子里,有三个Counter组件,还有一个统计三个Counter计数值之和的功能,我们遇到的麻烦就是这两者之间的状态如何同步的问题,现在,我们创造两个Store,一个是为Counter组件服务的CounterStore,另一个就是为总数服务的SummaryStore。

src/store/CounterStore.js,代码如下:

const counterValues = {

'First':0,

'Second':10,

'Third':30

}

const CounterStore = Object.assign({},EventEmitter.prototype,{

getCounterVlues:function(){

return counterValues;

},

emitChange : function(){

this.emit(CHANNGE_EVENT);

},

addChangeListener:function(callback){

this.on(CHANGE_EVENT,callback);

},

removeChangeListener:function(callback){

this.removeListener(CHANGE_EVENT,callback);

}

});

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

①emit函数,可以广播一个特定事件,第一个参数是字符串类型的事件名称。

②on函数,可以增加一个挂在这个EventEmitter对象特定事件上的处理函数,第一个参数是字符串类型的事件名称,第二个参数是处理函数。

③removeListener函数,和on函数做的事情相反,删除挂在这个EventEmitter对象特定事件上的处理函数,和on函数一样,第一个参数是事件名称,第二个参数是处理函数。要注意,如果要调用removeListener函数,就一定要保留对处理函数的引用。

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

CounterStore函数还提供了一个getCounterValues函数,用于让应用中其他模块可以读取当前的计数值,当前的计数值存储在文件模块级的变量counterValues中。

上面实现的Store只有注册到Dispatcher实例上才能真正发挥作用,所以还需要增加下列代码:

import AppDispatcher from '../AppDispatcher.js';

CounterStore.dispatcherToken =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函数的返回值保存在CounterStore对象的dispatcherToken字段上,待会就会用得到。

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

比如通过Dispatcher派发一个动作,代码如下:

AppDispatcher.dispatcher({

type:ActionTypes.INCREMENT,

counterCaption:'First'

});

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

作为一个普遍接受的传统,action对象中必有一个type字段,类型是字符串,用于表示这个action对象是什么类型。

在我们的例子中,action对象的type和counter Caption字段结合在一起,可以确定是哪个计数器应该做加一或减一的动作,上面例子中的动作含义就是:“名字为First的 计数器要做加一动作”。

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

接下来,我们再来看看另一个Store,也就是代表所有计数器计数值总和的Store,在src/stores/SummaryStore.js中编写源代码。

SummaryStore也有emitChange、addChangeListener还有removeChangeListener函数,功能一样也是用于通知监听者状态变化,这几个函数的代码和CounterStore中 完全重复,不同点是对获取状态函数的定义,代码如下:

  function  computeSummary(counterValues){

let summary = 0;

for(const key in counterValues){

if(counterValues hasOwnPoperty(key)){

summary+=counterVlues(key);

}

}

return summary;

}

const SummaryStore = Object.assign({},EventEmitter.prototype,{

getSummary:function(){

return computeSummary(CounterStore.getCounterValues());

}

});

可以注意到,SummaryStore并没有存储自己的状态,当getSummary被调用时,它是直接从CounterStore里获取状态 计算的。

CounterStore提供了getCounterValues函数让其他模块能够获得所有计数器的值,SummaryStore也提供了getSummary让其他模块可以获得所有计数器当前值的总和。不过,既然总可以通过CounterStore.getCounterValues函数获取最新鲜的数据,SummaryStore似乎也就没有必要把计数器当前值总和存储到某个变量里。事实上,可以看到SummaryStore并不像CounterStore一样用一个变量counterValus存储数据,SummaryStorer不存储数据,而是每次对getSummary的调用,都实时读取CounterStore.getCounterValus,然后实时计算出总和返回给调用者。

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

SummaryStore在Dispatcher上注册的回调函数

也和CounterStore很不一样,代码如下:

SummaryStore.dispatcherToken = Dispatcher.register((action)=>{

    if((action.type===ActionTypes.INCREMENT)||

(action.type===ActionTypes.DECREMENT)){

AppDispatcher.waitFor([CounterStore.dispatchehToken]);

SummaryStore.emitChange();

  }

});

SummaryStore同样也通过AppDispatcher.register函数注册一个回调函数,用于接受派发的action对象。在回调函数中,也只关注INCREMENT和DECREMENT类型的action对象,并通过emitChange通知监听者,注意在这里使用了waitFor函数,这个函数解决的是下面描述的问题。

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

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

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

于是,解决这个问题就靠Dispatcher的waitFor函数。在SummaryStore的回调函数中,之前在CounterStore中注册回调函数时保存下来的dispatcherToken终于派上用场了。

Dispatcher的waitFor可以接收一个数组作为参数,数组中的每个元素都是一个Disppatcher.register函数的返回结果,也就是所谓的dispatcherToken。这个waitFor函数告Dispatcher,当前的处理必须要暂停,直到dispatcherToken代表的那些已注册回调函数执行结束才能继续。

我们知道,JavaScript是单线程的语音,不可能有线程等待这回事,这个waitFor函数当然并不是多线程实现的,只是在调用waitFor的时候,把控制权交给Dispatcher,让Dispatcher检查一下dispatcherToken代表的回调函数有没有被执行,如果已经执行,那就直接继续,如果还没有执行,那就调用dispatcherToken代表的回调函数之后waitFor才返回。

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

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

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

看起来,这似乎是一种浪费,但是这个设计让Flux的Dispatcher逻辑最简单话,Dispatcher的责任越简单,就越不会出现问题。毕竟,由回调函数全权决定如何处理actioon对象,也是非常合理的。

4. view

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

不过,话说回来,既然我们都使用上Flux了,除非项目有大量历史遗留代码需要利用,否则实在没有理由不用React来实现view。

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

①创建时要读取Store上状态来初始化组件内部状态。

②当Store上状态发生变化时,组件要立刻同步更新组件内部状态保持一致;

③View如果要改变Store状态,必须而且只能派发action。

最后让我们来看看例子中的View部分,为了方便管理,所有的View文件都放在src/views目录下。

先看src/vies/ControlPanel.js中的ControlPanel组件,其中render函数的实现和上一章很不一样,代码如下:

render(){

return(

<div style={ style }>

<Counter caption ="First"/>

<Counter caption ="Seond"/>

<Counter caption ="Third"/>

<hrr/>

<Summary />

</div>

);

}

可以注意到,和前面章节中的ControlPanel不同,Counter组件实例只有caption属性,没有initValues属性。因为我们把计数值包括初始值全都放到CounterStore中去了,所以在创造Counter组件实例的时候就没有必要指定initValue了。

接着看src/views/Counter组件,构造函数中初始化this.state的方式有了变化,代码如下:

constructor(props){

super(props);

this.onChange = this.onChange.bind(this);

this.onClickIncrementButton =this.onClickIncrementButton.bind(this);

this.onClickDecrementButton= this.onClickDecrementButton.bind(this);

this.state = {

count:CounterStore.getCounterValues()[props.caption]

}

}

在构造函数中,CounterStore.getCounterValues函数获得了所有计数器的当前值,然后把this.state初始化为对应caption字段的值,也就是说Counter组件store来源不再是prop,而是Flux的Store。

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

componentDidMount(){

CounterStore.addChangeListener(this.onChange);

}

componentWillUnmount(){

CounterStore.removeChangeListener(this.onCange);

}

onChange(){

const newCount = CounterStore.getCounterValues()[this.props.caption];

this.setState({count:newCount});

}

如上面的代码所示,在componentDidMount函数中通过CounterStore.addChangeListener函数监听了CounterStore的变化之后,只有CounterStore发生化,Couunter组件的onChange函数就会被调用。与componentDidMount函数中监听事件相对应,在componentWillUnmount函数中删除了这个监听。

接下来,要看React组件如何派发action,代码如下:

onClickIncrementButton(){

Action.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函数的地方,第一处是在构造函数中初始化this.state的时候,第二处是在响应CounterStorre状态变化的onChange函数中,同样一个Store状态,为了转换为React组件的状态,有两次重复的调用,这看起来似乎不是很友好。但是,React组件的状态就是这样,在构造函数中要对this.state初始化,要更新它就要调用this.setState函数。

有没有更简洁的办法?比如说只使用CounterStore.getCounterValues一次?可惜,只要我们想用组件的状态来驱动组件的渲染,就不可避免要有这两步。那么如果我们不利于用组件的状态呢?

如果不使用组件的状态,那么我们就可以逃出这个必须在代码中使用Store两次的宿命,在接下来的章节里,我们会遇到这种“无状态”组件。

Summary组件,存在于src/views/Summary.js,和Counter类似,在constructor中初始化组件状态,通过在componentDidMount中添加SummaryStore的监听来同步状态,因为这个View不会有任何交互功能,所以没有派发出任何action。

3.1.3Flux的优势

本章例子和第二章只用React的实现效果一样,但是工作方式有了大变化。

回顾一下完全只使用React实现的版本,应用的状态数据只存在于React组件之中,每个组件都要维护驱动自己渲染的状态数据,单个组件的状态还好维护,但是如果多个组件之间的状态有关联,那就麻烦了。比如Counter组件和Summary组件,Summary组件要维护所有Counter组件计数值的总和,Counter组件和Summary分别维护自己的状态,如何同步Summary和Counter状态就成了问题,React只提供了props方法让组件之间通信,组件之间关系稍微复杂一点,这种方式就显得非常笨拙。。

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

在完全只用React实现的版本里,用户的交互操作,比如点击“+“按钮,引发的时间处理函数直接通过this.setState改变组件的状态。在Flux的实现版本里,用户的操作引发的是一个“动作“的派发,这个派发的动作会发送给所有的Store对象,引起Store对象的状态改变,而不是直接引发组件的状态改变。因为组件的状态是Store状态的映射,所以改变了Store对象也就触发了React组件对象的状态改变,从而引发了界面重新渲染。

Flux带来了什么好处呢?最重要的就是“单向数据流”的管理方式。

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

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

这看起来是一个“限制”,但却是一个很好的“限制”,禁绝了数据流混乱的可能。

简单来说,在Flux的体系下,驱动界面改变始于一个动作的派发,别无他法。

3.1.4Flux的不足

任何工具不可能只有优点没有缺点,接下来让我们看看Flux的不足之处,只有了解了Flux的不足之处,才能理解为什么会出现Flux的改进框架Redux。

1.Store之间依赖关系

在Flux的体系中,如果两个Store之间有逻辑依赖关系,就必须用上Dispatcher的waitFor函数。在上面的例子中我们已经使用过waitFor函数,SummmarryStore对action类型的处理,依赖于CounterStore已经处理过了。所以,必须要通过waitFor函数告诉Dispatcher,先让CounterStorer处理这些action对象,只有CounterStore搞定之后SummaryStore才继续。

那么,SummaryStore如何标识CounterStore呢?靠的是register函数的返回值dispatchToken。

而dispatcherToken的产生,当然是CounterStore控制的,换句话说,要这样设计:

□ CounterStore必须要把注册回调函数产生的dispatchToken公布与众。

□ SummaryStore必须要在代码里建立对CounterStore的dispatchToken的依赖。

虽然Flux这个设计的确解决了Store之间的依赖关系,但是,这样明显的模块之间的依赖,看着还是让人感觉不太舒服,毕竟,最后的依赖管理是根本不让依赖产生的。

2.难以进行服务器端渲染

关于服务器端渲染,我们在后面的章节(第12章)“同构”中详细介绍,在这里,我们只需要知道,如果要在服务器端渲染,输出不是一个DOM树,而是一个字符串,准确来说就是一个全是HTML的字符串。

在Flux的体系中,有一个全局的Dispatch,然后每一个Store都是一个全局唯一的对象,这对于浏览器端应用完全没有问题,但是如果放在服务器端,就会有大问题。

和一个浏览器只服务于一个用户不同,在服务器端要同时接受很多用户的请求,如果每个Store都是全局唯一的对象,那不同请求的状态肯定就乱套了。

并不是说Flux不能做服务器端的渲染,只是说Flux做服务器端渲染很困难,实际上,Facebook也说的很清楚,Flux不是设计用作服务器端渲染的,他们也从来没有尝试过把Flux应用于服务器端。

3.Store混杂了逻辑和状态

Store封装了数据和处理数据的逻辑,用面向对象思维来看,这是一件好事,毕竟对象就是这样定义的。但是,当我们需要动态替换一个Store的逻辑时,只能把这个Store整体替换掉,那也就无法保持Store中存储的状态。

读者可能会问,有什么场景是需要替换Store的呢?

在开发模式下,开发人员要不停的对代码进行修改,如果Store在某个状态下引发了bug,如果能在不毁掉状态的情况下替换Store的逻辑,那就最好了,开发人员就可以不断地改进逻辑来验证这个状态下bug是否修复了。

还有一些应用,在生产环境下就要根据用户属性来动态加载不同的模块,而且动态加载模块还希望不要网页重新加载,这时候也希望能够在不修改应用状态的前提下重新加载应用逻辑,这就是热加载(Hot Load),在第12章会详细介绍如何实现热加载。

可能读者会觉得这里所说的“偷梁换柱”一样的替换应用逻辑是不能做到的。实际上,真的能够做到,Redux就能做到,所以让我们进入Redux的世界吧。

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

推荐阅读更多精彩内容