前端技术栈:从Flux到Redux

Posted TurkeyCock

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端技术栈:从Flux到Redux相关的知识,希望对你有一定的参考价值。

上一篇分析了Flux出现的背景和原理,最核心的思想就是“组件化+单向数据流”。

但是,Flux在设计上并非完美,具体来说主要存在以下2个不足:

1. 多Store数据依赖

由于Flux采用多Store设计,各个Store之间可能存在数据依赖。以flux-chat为例:在这个聊天软件里,可能会有多个人给你发消息,比如Dave给你发了3条,Brian给你发了2条,当你点开某个人给你发的消息后,界面需要刷新,显示你目前还有几个人的未读消息没有查看:

为了解决这个需求,创建了3个Store:

  • ThreadStore用来存储消息组状态
  • MessageStore用来存储每个组里的消息的状态
  • UnreadThreadStore用来计算目前还有几个消息组没有查看

当你点开某个消息组时,显然你需要先更新ThreadStore和MessageStore,然后再更新UnreadThreadStore。由于Store的注册顺序是不确定的,为了应付这种依赖,Flux提供了waitFor()机制,每个Store在注册之后都会生成一个令牌(dispatchToken),通过等待令牌的方式确保其他Store被优先更新。

因此UnreadThreadStore的代码会写成下面这个样子:

Dispatcher.waitFor([
  ThreadStore.dispatchToken,
  MessageStore.dispatchToken
]);

switch (action.type) 
  case ActionTypes.CLICK_THREAD:
    UnreadThreadStore.emitChange();
    break;
  ...

虽然可以工作,但是总觉得不是很优雅,在一个Store中需要显示地包含其他Store的调用。当然你会说,干脆把这3个Store的代码糅到一起,搞成一个Store不就行了?但是这样又会导致代码结构不够清晰,不利于多模块分工协作。

为了兼顾这两个方面,Redux使用全局唯一Store,外部可以使用多个reducer来修改Store的不同部分,最后会把所有reducer的修改再组合成一个新的Store状态。

2.状态修改不是纯函数

所谓纯函数,是指输出只和输入相关,相同的输入一定会得到相同的输出。用专业一点的术语来说,纯函数没有“副作用”。我们先来看看Flux中是怎么修改状态的:

Dispatcher.register(action => 
  switch(action.type) 
    case ActionTypes.CLICK_THREAD:
      _currentID = action.threadID;
      ThreadStore.emitChange();
      break;
    ...

可以看到,是直接修改变量值,然后显式发送一个change事件来通知View。

我们再来看看Redux中是怎么修改状态的:

export default function threadReducer(state = , action) 
  switch (action.type) 
    case ActionTypes.CLICK_THREAD: 
      return  ...state, _currentID: action.threadID ;
    ...

细心的人可能已经看出来了,主要有3点区别:

  • 前面的函数里只有一个action参数,而这里多了一个state参数
  • 不是直接修改state中的字段,而是需要返回一个新的state对象
  • 不需要显式发送事件通知View,实际上,Redux内部会检测state对象的引用是否发生了变化,然后自动通知View进行刷新

那么有人会说了,为啥要这么做,好像也没看到啥好处嘛?当然是有好处的,这样可以支持“时间旅行调试(Time Travel Debugging)”。所谓时间旅行调试,指的是可以支持状态的无限undo / redo。由于state对象是被整体替换的,如果想回到上一个状态重新执行,那么直接替换成上一步的state对象就可以了。

3.什么是Redux?

首先我们要搞清楚,Redux解决了哪些问题?主要是以下3点:

1.如何在应用程序的整个生命周期内维持所有数据?

Redux是一个“状态容器”。写过React或者ReactNative的同学可能会有感受,如果多个页面需要共享数据时,需要把数据一层层地传递下去,非常繁琐。如果能有一个全局统一的地方存储数据,当数据发生变化时自动通知View刷新界面,是不是很美好呢?因此,我们需要一个“状态容器”。

2.如何修改这些数据?

Redux借鉴了分布式计算中的map-reduce的思想,把Store中的数据分割(map)成多个小的对象,通过纯函数修改这些对象,最后再把所有的修改合并(reduce)成一个大的对象。修改数据的纯函数被称为reducer

3.如何把数据变更传播到整个应用程序?

通过订阅(subscribe)。如果你的View需要跟随数据的变化动态刷新,可以调用subscribe()注册回调函数。在这一点上,Redux是非常粗粒度的,每次只要有新的action被分发,你都会收到通知。显然,你需要对通知进行过滤,这意味着你可能会写很多重复代码。不过,这也是出于通用性和灵活性考虑,实际上Redux不仅可以用于React,也可以用在Vue.js或者Angular上。可以搭配特定框架相关的适配层比如react-redux来规避这些重复代码。

说了这么多,我们来看一下Redux的基本框架:

和前一篇的Flux框架图对比一下可以发现,Redux去除了dispatcher组件(因为只有一个Store),增加了recuder组件(用于更新Store的不同部分)。下面详细介绍各个部分的作用。

4.Redux基本概念

4.1 Store

首先我们需要创建一个全局唯一的Store,Redux提供了辅助函数createStore():

import  createStore  from 'redux'
var store = createStore(() => )

你可能注意到了,createStore()需要提供一个参数,这个参数就是reducer。

4.2 Reducer

前面介绍过,reducer就是一个纯函数,输入参数是state和action,输出新的state。一般的代码模板如下:

var reducer = (state = , action) => 
	switch (action.type) 
	case 'MY_ACTION': 
    	return ...state, message: action.message
    default:
    	return state
    

需要注意的是,default分支一定要返回state,否则会导致状态丢失。

好了,现在我们有了reducer,可以作为参数传递给4.1节中的createStore()函数了。

createStore()只能接受一个reducer参数,如果我们有多个reducer怎么办?这时需要使用另一个辅助函数combineReducers():

import  combineReducers  from 'redux'
var reducer = combineReducers(
    first: firstReducer,
    second: secondReducer
)

combineReducers()会把多个reducer组合成一个,当有action过来时会依次调用每个子reducer,所以实际上你可以组织成一个树状结构。

4.3 Action

所谓action,其实就是一个普通的javascript对象,一般会包含一个type属性用于标识类型,以及一个payload属性用于传递参数(名字可以随便取):

var action = 
    type: 'MY_ACTION',
    payload:  message: 'hello' 

那么如何发送action呢?store提供了一个dispatch()函数:

store.dispatch(action)

4.4 Action Creator

所谓action creator,其实就是一个用来构建action对象的函数:

var actionCreator = (message) => 
	return 
        type: 'MY_ACTION',
        payload:  message: message 
	

所以4.3节发送action的代码也可以写成这样:

store.dispatch(actionCreator('hello'))

4.5 状态读取和订阅

当你发送了一个action,reducer被调用并完成状态修改,那么前端视是怎么感知到状态变化的呢?我们需要通过subscribe()进行订阅:

store.subscribe(() => 
	let state = store.getState()
	... ...
)

store的getState()函数可以获得当前状态的一个副本,然后就可以刷新界面了,以React为例,可以调用this.setState()或者this.forceUpdate()触发重新渲染。

当视图组件比较多时,每次都要写这段订阅代码会比较繁琐,后面会介绍通过react-redux来简化这一过程。

4.6 Middleware

第3章的那张图其实还少画了个东西,叫做middleware(中间件)。那么这个middleware是干什么用的呢?

在Web应用中经常会有异步调用,比如请求网络、查询数据库什么的。我们首先发送一个action启动异步任务,并希望在异步任务完成以后再更新状态,应该如何实现呢?在Flux中,我们可以在dispatcher里完成:首先启动异步任务,然后在回调函数中再发送一个新的action去更新Store。但是Redux中去除了dispatcher的概念,你能调用的只有store的dispatch()函数而已,那我们该怎么办呢?答案就是middleware。

所以,Redux的完整流程应参见下面这张动图:

我们先来看一个简单的middleware的例子:

var thunkMiddleware = ( dispatch, getState ) => 
    return (next) => 
        return (action) => 
            return typeof action === 'function' ?
                action(dispatch, getState) :
                next(action)
        
    

可以发现,其实middleware就是一个三层嵌套的函数:

  • 第一层向其余两层提供dispatch和 getState 函数
  • 第二层提供 next 函数,它允许你显式的将处理过的输入传递给下一个middleware或 reducer
  • 第三层提供从上一个中间件或从 dispatch 传递来的 action

所以,实际上middleware可以理解在action进入reducer之前进行了一次拦截。在这个例子里,如果action是一个函数,我们就不会把action继续传递下去,而是调用这个函数去执行异步任务。当异步任务执行完毕后,我们可以调用dispatch()函数发送一个新的action,用于调用reducer更新状态。

那么我们如何注册一个中间件呢?Redux提供了一个工具函数applyMiddleware(),可以直接作为createStore()的一个参数传递进去:

const store = createStore(
  reducer,
  applyMiddleware(myMiddleware1, myMiddleware2)
)

预告一下,后面一篇要介绍的redux-saga,其实就是一个Redux中间件。

5.使用react-redux

Redux的设计主要考虑的是通用性和灵活性,如果想更好的配合React的组件化编程习惯,你可能需要react-redux。

Redux使用全局唯一的Store,另外当你需要发送action的时候,必须通过store的dispatch()函数。这对于一个有很多页面的React应用来说,意味着只有两种选择:

  • 在所有页面中import全局store对象
  • 通过props把store对象一层一层地传递下去

这显然极其繁琐,幸运的是,React提供了Context机制,说白了就是所有页面都能访问的一个上下文对象:

react-redux利用React的Context机制进行了封装,提供了<Provider>组件和connect()函数来实现store对象的全局可访问性。

5.1 <Provider>

这是一个React组件,使用时需要把它包裹在应用层根组件的外面,然后把全局store对象赋值给它的store属性

import  Provider  from 'react-redux'
import store from './mystore'
export default class Application extends React.Component 
  render () 
    return (
      <Provider store= store >
        <Home />
      </Provider>
    )
  

5.2 connect()

Provider组件只是把store对象放进了Context中,如果你需要访问它,还需要一些额外的代码,react-redux提供了一个connect()函数来帮你完成这些工作。

实际上,connect()就帮你做了两件事:

  • 在你的组件外面包装了<Context.Consumer>组件,获取Context中的store对象
  • 根据你提供的selector函数,帮你把state中的值以及store.dispatch()函数映射到props中,这样在代码中你就可以直接通过this.props.xxx进行访问了

实现层面上,connect()采用了React的HOC(高阶组件)技术,动态创建新组件及其实例:

那么这个connect()怎么用呢?我们通过3个应用场景依次介绍。

1.你只是希望能在组件中使用dispatch()直接派发action

这是最简单的情况,你只需要在导出组件的时候加上connect()就可以了:

export default connect()(MyComponent)

当你需要派发action的时候,可以直接调用this.props.dispatch()。

2.你不想直接使用dispatch(),希望能够自动派发action

实际上你会发现,如果action很多的话,你需要不停地调用dispatch()函数。为了使我们的实现更加“声明式”,最好是把派发逻辑封装起来。实际上Redux中有一个辅助函数bindActionCreators()来完成这项工作,它会为每个action creator生成同名的函数,自动调用dispatch()函数:

const increment = () => ( type: "INCREMENT" );
const decrement = () => ( type: "DECREMENT" );
const boundActionCreators = bindActionCreators( increment, decrement , dispatch);
// 返回值:
// 
//   increment: (...args) => dispatch(increment(...args)),
//   decrement: (...args) => dispatch(decrement(...args)),
// 

这样你就可以直接调用boundActionCreators.increment()派发action了。那么如何跟connect()联系起来呢?这里需要用到它的第2个参数(第1个参数后面再介绍)mapDispatchToProps,举个例子:

const mapDispatchToProps = (dispatch) => 
  return bindActionCreators( increment, decrement , dispatch);


export default connect(null, mapDispatchToProps)(MyComponent)

这样,你就可以在组件中直接调用this.props.increment()函数了。

你以为这样就结束了?还有更简单的方法,连bindActionCreators()都不用写!你可以直接提供一个对象,包含所有的action creator就行了(这被称为“对象简写”方式):

const mapDispatchToProps =  increment, decrement 
export default connect(null, mapDispatchToProps)(MyComponent)

注意:如果你提供了mapDispatchToProps参数,那么默认情况下dispatch就不会再注入到props中了。如果你还想使用this.props.dispatch(),可以在mapDispatchToProps的返回值对象中加上dispatch属性。

3.你希望访问store中的数据

这应该是使用最多的场景,组件访问store中的数据并刷新界面。根据“无状态组件”设计原则,我们不应该直接访问store,而需要通过一个“selector函数”把store中的数据映射的props中进行访问,这个“selector函数”就是conntect()的第1个参数mapStateToProps。举个例子:

const mapStateToProps = (state = , ownProps) => 
  return 
    xxx: state.xxx
  


export default connect(mapStateToProps)(MyComponent)

这样你在组件中就可以通过this.props.xxx进行访问了。另外,它还会帮你自动订阅store,任何时候store状态数据发生变化,mapStateToProps都会被调用并导致界面重新渲染。除了第一个参数state之外,还有一个可选参数ownProps,如果你的组件需要用自身的props数据到store中检索数据,可以通过这个参数获取。

当然,你可以同时提供mapStateToProps和mapDispatchToProps参数,这样你就可以获得两方面的功能:

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent)

最后,以一张思维导图结束本篇文章,下一篇介绍redux-saga。

更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.csdn.net/turkeycock
或关注飞久微信公众号:

以上是关于前端技术栈:从Flux到Redux的主要内容,如果未能解决你的问题,请参考以下文章

图书推荐React全栈:Redux+Flux+webpack+Babel整合开发

彻底征服 React.js + Flux + Redux

第3章 从Flux到Redux

从Flux到Redux详解单项数据流

redux+flux(一:入门篇)

React:如何从 Redux/Flux 操作访问组件引用?