理解Redux数据流

4,391 阅读7分钟

对于刚刚接触redux的前端开发者来说,要想一下子直接理解redux内部的数据流向可能有点不太现实。但经过一段时间的总结和上手一个简单的redux项目,相信我们对redux内部的数据流向及内部原理就能够掌握得比较清楚了。

Redux简介

现在我们就从Redux是什么,Redux的三大原则和Redux的核心API开始介绍Redux,并说明Redux如何与React结合使用,以及它在Flux基础上的改变。

Redux是什么

我们都知道Flux它本身既不是库,也不是框架,而是一种应用的架构思想。而Redux呢,它的核心代码可以理解成一个库,但同时也强调和Flux类似的架构思想。
从设计上看,Redux参考了Flux的设计,但是对Flux许多冗余的部分做了简化,同时将函数式编程的思想融合其中。
非常有意思的是,Redux是从一个实验开始的,作者并没有想到Redux会变得如此重要又被广泛使用,他只是为了通过Flux思想解决他的热重载及时间旅行的问题而已。
Redux本身非常简单,它的设计思想与React有异曲同工之妙,均是希望用最少的API实现最核心的功能。Redux本身只把自己定位成一个可预测的状态容器
“Redux”本身指redux这个npm包,它提供若干API让我们使用reducer创建store,并能够更新store中的数据或获取store中最新的状态。而“Redux应用”则是指使用了redux这个npm包并结合了视图层实现(如React)及其他前端应用必备组件(路由库,请求库等)组成的完整的类Flux思想的前端应用。

Redux三大原则

想要理解Redux,必须要知道Redux设计和使用的三大原则。

  • 单一数据源
    在传统的MVC框架中,我们可以根据需要创建无数个Model,而Model之间可以互相监听、触发事件甚至循环或嵌套触发事件,这些在Redux中都是不允许的。
    因为在Redux的思想里,一个应用永远只有唯一的数据源。我们的第一反应可能是:如果一个复杂应用,强制要求唯一的数据源岂不是会产生一个特别庞大的JavaScript对象。
    实际上,使用单一的数据源的好处在于整个应用状态保存在一个对象中,这样我们随时可以提取出整个应用的状态进行持久化。此外,这样的设计也为服务端渲染提供了可能。
    至于我们担心的数据源对象过于庞大的问题,我们可以通过combineReducers化解。

  • 状态是只读的
    在Redux中,我们并不会自己用代码来定义一个store。取而代之的是,我们定义一个reducer,它的功能是根据当前触发的action对当前应用的状态进行迭代,这里我们并没有直接修改应用的状态,而是返回了一份全新的状态。
    Redux提供的createStore方法会根据reducer生成store。最后,我们可以利用store.dispatch方法来达到修改状态的目的。

  • 状态修改均由纯函数完成
    在Redux里,我们通过定义reducer来确认状态的修改,而每一个reducer都是纯函数,这意味着它没有副作用,即接受一定的输入,必定会得到一定的输出
    这样设计的好处不仅在于reducer里对状态的修改变得简单、纯粹、可测试,更有意思的是,Redux利用每次新返回的状态生成酷炫的时间旅行调试方式,让跟踪每一次因为触发action而改变状态的结果成为了可能

Redux核心API

Redux的核心是一个store,这个store由Redux提供的createStore方法生成。要想生成store,必须传入reducers。
在Redux里,负责响应action并修改数据的角色是reducer。reducer本质上是一个纯函数,它有两个参数state和action。reducer的职责就是根据state和action计算出新的state。
在实际应用中,reducer在处理state时,还需要有一个特殊的非空判断。很显然,reducer第一次执行的时候,并没有任何的state,而reducer的最终职责是返回新的state,因此需要在这种特殊情况下返回一个定义好的state。这也就是我们会定义一个defaultState的原因。
下面来介绍一下Redux中最核心的API--createStore:

import { createStore } from 'redux';
const store = createStore(reducers);

通过createStore方法创建的store是一个对象,它本身包含4个方法。

  • getState():获取store中当前的状态。
  • dispatch(action):分发一个action,并返回这个action,这是唯一能改变store中数据的方式。
  • subscribe(listener):注册一个监听者,它在store发生变化时被调用。
  • replaceReducer(nextReducer):更新当前store里的reducer,一般只会在开发模式中调用该方法。
    在实际开发中,我们最常用的是getState()和dispatch()这两个方法。至于subscribe()和replaceReducer()方法,一般会在Redux与某个系统做桥接的时候使用。

简单的项目实践

在当前的项目中,模块化开发已经成为主流。个人认为在模块化开发中,最重要的便是目录结构的设计。

在这里我们对目录进行了比较细致的划分:

  • components:该目录下存放的是组件,比如开发网站类的项目实经常用到的Header公共组件。
  • pages: 该目录下存放的是网页,系统中各个网页放在该目录下。比如首页和详情页等。
  • store:该目录存放actions、reducers、constants(常量文件:存放dispatch要用到的常量)和index(创建store)。

个人认为在Redux项目中这样的目录设计是比较符合模块化设计思想的目录。(仁者见仁智者见智)
接下来我就介绍一下Redux具体的步骤和流程。(已请求简单列表数据为例)

第1步:reducer

store中的数据是由reducer决定的,所以第一步先完成reducer。

const defaultState = {
  homeList: []
}
export default function(state = defaultState, action) {
  switch(action.type) {
    case GET_HOME_LIST:
      return {
        homeList: action.homeList
      };
    default:
      return defaultState
  }
}

第2步: action

export const getHomeList = {
  type: GET_HOME_LIST,
  // 模拟从后端接收到的假数据
  homeList: [0,1,2,3]
}

第3步: constant

一个应用的常量应该是唯一的,把常量放在一个文件夹里同时按照模块划分,有利于防止变量名的冲突。

// home
export const GET_HOME_LIST = 'GET_HOME_LIST';

第4步: index

该文件是用来创建store的。

import { createStore, 
  combineReducers} from 'redux'
import HomeReducer from './reducers/home/index';
// 多个 reducer 合并成一个
const rootReducer = combineReducers({
  home: HomeReducer
})
// 创建 store 只能接收到 一个 reducer 
// 所以 创建之前 合并一下
export default createStore(rootReducer)

第5步: 使用store

class App extends ImmutableComponent {
  render() {
    return (
      // store成为全局变量,任何组件都可以访问了
      <Provider store={store}>
        <BrowserRouter>
            <Header />
            <Route path="/" component={Home} exact/>
            <Route path="/detail" component={Detail} />
        </BrowserRouter>
      </Provider>
    );
  }
}
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from '../../store/actions/home';
class Home extends Component {
  componentDidMount() {
    this.props.fetchHomeList();
  }
  state = {}
  render() {
    return ( <div>
      home
      length: { this.props.homeList.length }
    </div> );
  }
}
// 获取数据
// state:整个store,home页面,只要home模块,过滤一下
// 过滤完结果(return)都会由 connect 传给你组件的 props
const mapStateToProps = (state) => {
  return {
    homeList: state.home.homeList
  }
}
// 用户操作UI引起页面变化
// 发起一个action
// dispacth行为connect传给组件
const mapDispatchToProps = (dispatch) => {
  return {
    fetchHomeList() {
      dispatch(getHomeList)
    }
  }
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);

希望这个流程能对我们学习Redux有一定的帮助。
[注] 以上代码只是一个Redux的流程,并不是完整的代码。