聊一聊Redux在React项目中的数据管理实现方式

2,165 阅读10分钟

前言

     在两年前曾短暂的使用过react+redux进行开发,可是一入vue深似海,从此其他是路人,趁着闲的时间,赶紧撸一把redux在react的实现。下面来简单的和大家分享一下。

什么是flux?

     在了解redux之前,有必要先了解一下flux, flux是Facebook用户建立客户端Web应用的前端架构,以单向数据流方式支持MVC, Flux应用有三个主要部分:Dispatcher调度,存储Store 和 视图View ,当一个用户和视图交互时,有一个分发器dispatcher用来发送动作action到数据存储中,然后更新视图,存储接受更新,适当地调节这些更新,而不是一致地依赖外部更新其数据,存储之外根本不知道它是如何管理领域数据的,这有助于实现一种清晰的分离关注

这是flux的结构和数据流向图,不难看出一个单向数据流是Flux模式的核心。

什么是redux?

     大家如果稍有了解或者曾经接触的话,一定会听过这样一句话:如果说flux是一种思想,那么redux就是其中一种实现。 当然还有其它的实现方式如MoBx等。

     Redux是对flux的一种演变,也是flux思想的一种子集。设计目的在于 Redux 试图让 state 的变化变得可预测,Redux使Javascript的状态管理变得更加可预期,以一种新方式思考开发应用,这个方式是:状态从一个初始状态开始,被一系列动作序列改变,这种新方式是通往复杂Web应用的捷径。

    这张图很详细的解释了redux的数据流向,对比flux可以看出,redux依旧是通过action来描述将要触发的行为, 没有dispatcher这个分发器了,当每次Action被触发需要dispatch时,使用一个函数称为reducer来返回新的应用状态。 redux保留的应用状态是不可变的。有兴趣可以了解一下 immutable.js这个概念。

redux的工作流程

redux的工作流程与其设计三大基本原则密不可分

  • 单一数据源
  • state是只读的
  • 只能使用reducer来修改

首先要明确一下在react中完整的redux流程需要那几个对象 或者角色。

  1. 组件/视图
  2. state 存储中心
  3. action 行为
  4. reducer 暂且叫它处理业务的地方或者过滤器

再来看它的工作原理

     上述 2,3,4的合集我们暂且将它称为store, 基本的角色有了,那么还得做一些角色之间的关联,比如组件,如果需要访问store里面的数据,那么首先就要和store做链接,只有建立连接之后那么他才有一个访问权。如果组件想要访问store里面的state,那么然后他要定义一个动作类型,包含一个名称和一些便于store识别的信息(可选),根据这个动作类型就是action, store会把他丢给reducer, reducer是store的大脑,可以访问store中的元数据,然后根据store丢过来的信息,reducer做出种种处理,最后返回一个结果。这个结果再由store传递给视图方面,来使页面更新。

如果上面的这个阐述的不够清楚的话,大家可以想象一个模拟借书的场景 把各个角色代入进去,大致是这样的,

component/view ====》 借书人小王

state====》图书馆A

action====》是欲借的书的信息(三国演义)

reducer====> 图书馆管理员(易中天)

角色暂时这样安放,那么把场景带入,

     话说中国历史博大精深,小王同学从小最爱学历史,而且对三国的历史最感兴趣,于是乎小王周五放学拿着借书卡就往国图书馆奔, (ps:借书人首先要在该图书馆办一个借书卡,图书馆见卡才能放行) 走到门口,门卫一看有借书卡,来的还是个小伙子,还夸了句:“小伙子年纪轻轻就这么爱学习,好啊 真好啊。” 小王抿嘴一笑, 不置可否,径直借书去了,可是走进去,小王蒙了,这么多书,我找一本三国演义,我太难了,此时小王灵机一动忽然想起,这里头的图书馆管理员,易中天,江湖人称老易,掌管整个书馆,绝对能找到,于是找到老易,老易是一个七八十岁老头,一脸博学的文化人,看见小王也很高兴,说小伙子,这回借什么书,小王说:“易爷爷,这次想借一本三国演义”,老易说:“三国演义啊,行 等我想想在哪”,约莫4,5秒,老易径自拿出一本三国演义给了小王,小王大喜连声道谢。于是捧书而走。 埋头苦读,遂成三国通。

整个场景与上面的阐述大家可以思考一下,上面主要是讲一下概念,大家最关心的肯定是代码实现 下面分步骤来解释.

代码实现

拿简书网站的这个部分来演示一下实现

1. 定义组件

这是定义的上面那个列表的组件,那么组件写好之后,我们就开始往里头填数据,数据从何而来,自然是redux

2.定义元数据

reducer.js 内容如下:
import { fromJS } from 'immutable';
import * as constants from './constants';
const defaultState = fromJS({
	writerList: [],
});

这个writerList就是我们最后要渲染到WriterWrapper里面的数据,接下来的一切行为都是为了取到这个writerList

3.链接组件与store

WriterWrapper组件添加如下内容
import React, { PureComponent } from 'react';
import {connect} from 'react-redux'
const mapStateToProps = (state) =>{
	return {
           list: state.getIn(['home', 'writerList'])   // 这一步取到为空 动作还未触发
	}
}
const mapStateTodispatch = (dispatch) => {
	return {
	
	}
}
export default connect(mapStateToProps, mapStateTodispatch)(Writer);

     react-redux抛出了一个connect方法 用来将react组件和store做绑定链接,connect方法接受两个参数,一个是mapStateToProps,这个参数的作用是根据组件自身的状态选择自身需要的数据,因为这个方法它可以访问store里面所有的元数据,一旦使用错误 会造成性能上的损耗,所以要谨慎使用,另一个是mapStateTodispatch,包含一个dispatch方法,负责将视图的行为也就是action传递给reducer进行处理,最后跟上组件名称则表示组件已经和redux进行了关联,为什么用这种方式就能关联,先跳过,我们稍后再详细说明。

4.定义action 触发

     到这里需要明确两件事: 1.我要拿到什么。 2通过什么方式拿 dispatch派发的action一般情况下是一个对象,但是当涉及到异步时 action也可以是一个函数,典型的例子就是上面这个writerList我们需要从服务端取得,这个时候我们可能需要定义一个函数类型的action代码如下:

import { actionCreators } from '../store/index'
const mapStateToProps = (state) =>{
  return {
        list: state.getIn(['home', 'writerList'])
	}
}
const mapStateTodispatch = (dispatch) => {
	return {
		getWriterList(){
			dispatch(actionCreators.getWriterList())
		}
	}
}
componentDidMount(){
	this.props.getWriterList()
}

     list就是我们最终要取得的数据,我们需要通过getWriterList() 这个方法去获取数据,因为已经做了关联, 我们可以在生命周期钩子里去调用这个函数 此时dispatch触发的是一个异步函数 actionCreators.js文件中的getWriterList()方法 为了方便管理 可以都统一拆分到一个actionCreators.js文件之中然后抛出 其代码如下:

actionCreators.js内容如下
export const getWriterList = () => {
// 借用redux-chunk可以使action处理异步  标记是返回的是否是函数
   return (dispatch) => {
      axios.get('/api/writerList.json').then(res=>{
				const result  = res.data.data;
				dispatch(addWriterList(result))
			}).catch(err=>{
				console.log(err)
			})
	 }
}

     很明显在这里,我们发出了一个请求, 将返回的结果再次dispatch触发 此时将结果传入到一个addWriterList()方法中,代码如下:

export const ADD_WRITER_LIST = 'home/ADD_WRITER_LIST'; constants.js

actionCreators.js内容添加方法
import * as constants from './constants';
const addWriterList = (result) => {
	return {
		type: constants.ADD_WRITER_LIST,
		writerList: fromJS(result.writerList)
	}
}

     这个方法最后return出一个对象 符合了非异步情况下action一般是一个object的规则 是不是好奇这个constants,其实就是定义的actionType,reducer需要根据这个type来做出相应处理。

5.reducer过滤

reducer是一个纯函数,本质上传入一定类型的数据,必然返回一定类型的结果,做具体的数据过滤尤其合适,

reducer.js内容如下
const defaultState = fromJS({
	writerList: [],
});
 const addWriterList = (state, action)=>{
  return state.set('writerList', action.writerList)
}
 export const ADD_WRITER_LIST = 'home/ADD_WRITER_LIST'; constants.js
 export default (state = defaultState, action) => {
	switch(action.type) {
		case constants.ADD_WRITER_LIST:
			return addWriterList(state, action);
		default:
			return state;
	}
}

     reducer接受两个参数 第一个是state元数据, 第二个是action视图触发的动作类型,通过这两个信息 reducer就可以返回符合期望的信息,参照所有图书和三国演义的例子。根据对应的ADD_WRITER_LIST类型 执行完addWriterList方法后 此时 defaultState 的 writerList, 已经是一个经过服务端数据填充的元数据,而因为组件与redux做了链接 所以页面会正确的渲染出目标视图,即简书网站的列表组件。

怎么做的链接?

大家可能一直有疑问为什么通过connect方法就能和组件做链接, connect背后的逻辑是这样的

1.创建一个全局store对象

import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(
	applyMiddleware(thunk)
));
export default store;

     类似上面的代码大家肯定都很熟悉,可以先不管引入的reducer,只需要知道是处理具体的过滤逻辑即可,是一个庞大的模块,从上面的redux模块中 引入了createStore这个方法并执行, applyMiddleware是作为中间件处理异步,thunk可以使传入的action不仅仅是对象, 还可以是函数,执行createStore()方法后把它抛出。 那我们再来看reducer,reducer是一个庞大的模块,那么必然是有一个个小的模块去组成的,每一个小的模块,又包含 redux的各种角色。这种拆分方式有利于各个模块之间的数据管理不被污染

假如将我们刚才做的WriterWrapper组件看作一个小的store 那么它的结构现在是这样的

index.js内容如下
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
这个文件引入了 刚才所需要的所有内容 并做了抛出  我们姑且把它放到home目录下。

2 .创建一个全局reducer文件

import { combineReducers } from 'redux-immutable';
import { reducer as homeReducer } from '../pages/home/store';
const reducer = combineReducers({
	home: homeReducer,
})
export default reducer;

这个文件引入了刚才抛出的那个home目录下的reducer对象,介绍一下combineReducers是为了做多个reducer的合并

3.绑定视图

import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header';
import Home from './pages/home';
import store from './store';

class App extends Component {
  render() {
    return (
    	<Provider store={store}>
      	<BrowserRouter>
      		<div>
            <Header />
      			<Route path='/' exact component={Home}></Route>
      		</div>
      	</BrowserRouter>
      </Provider>
    );
  }
}
export default App;

     通过react-redux的provider提供商 将整个容器包裹,于是就相当于整个store和应用做了链接,这时就有了组件的connect链接,这样一个完整的redux流程就基本形成了。

写在最后

    一直纠结于文章的顺序安排,有些地方顺序安排的不合理,ps:(千万别误人子弟了)大家也可以给出自己的见解,我会继续补充修改本文。

文章参考:

www.jdon.com/idea/flux.h…

cn.redux.js.org/

yq.aliyun.com/articles/66…