React: 研究Redux的使用

一、简介

在上一篇文章中,大概讲了下Flux设计模式的使用,在末尾顺便提了一些基于Flux的脚本库,其中Redux已经毋庸置疑地成为了众多脚本库的翘楚之一。是的,Redux是基于Flux开发的,Redux库的尺寸非常小,代码量少,核心目的仍是处理数据流变化的问题。Redux并不完全是Flux,它只是类Flux的脚本库,它移除了Dispatcher这个概念,现在的结构是包含Store、Action、Action生成器、以及用于修改State的Action对象。在Redux中,对于State的管理采用了单个不可变对象来表示,同时还引入了Reducer纯函数来根据当前的State和Action返回一个新的State:(state, action) => newState。现在来对每一个成分结构进行分析。

 

二、State

 在传统的React开发中,将State分布到每一个组件中基本上是通用的做法,这么做的好处是明显的,每个State数据由自己的组件去管理,需要那个组件做修改就让对应的组件去处理,但是,随着数据的增加,弊端也是明显的,管理应用程序的整体State可能就变得异常困难,因为要考虑到每个组件都将使用其内部的setState方法来改变自身的State,可能了解更新命令源也会变得更加复杂。然而,在Redux中则推荐将State尽量存放到少数几个对象中,这中处理方式几乎成了一条规则。现在来对比这两种不同的State管理分布,如下所示。可以发现Redux将所有的State数据都存储到了单个对象中,简化了在应用程序中查看和修改State的方式。


通过Redux,使得State管理与React完全剥离了,Redux将会直接管理这些State。从图中可以看出,Redux Store中所有的数据都植根于一个对象:State树。单个对象中的每一个键表示State树的一个分支。用图可以形象地表示如下:

用代码表示如下:

//state_data.js

//
存储的数据
//注意事项:后面本示例不处理kind对象,将subs补全为subjects
const state_data = { names:[ { "name_id":"1", "name":"张三", }, { "name_id":"2", "name":"李四", } ], subjects:[ { "subject_id":"1", "subject":"数学", }, { "subject_id":"2", "subject":"语文", } ], scores:[ { "score_id":"1", "score": 90, }, { "score_id":"2", "score": 95, } ] };

 

三、Action

根据Redux的规则,在上面已经确定了应用程序State的基本结构,数据都存放到了一个不可变的对象data中。不可变意味着data对象不可修改,此时只能通过整体替换的方式来更新data对象进而达到更新State的目的。在前面Flux中说过,Action是更新应用程序State的唯一方式,在Redux也是如此。对于State的每一种更新行为Action都是抽象的,开发者可以确认完Action后将其动词化。它的类型是字符串,通常由大写字母表示,并用下划线代替空格,做到见名知意为好。如下所示:

//action_constants.js

//将Action行为动词化 const action_constants = { ADD_NAME:"ADD_NAME", //添加姓名 DELETE_NAME:"DELETE_NAME", //删除姓名 UPDATE_NAME:"UPDATE_NAME", //更新姓名 ADD_SUBJECT:"ADD_SUBJECT", //添加科目 DELETE_SUBJECT:"DELETE_SUBJECT", //删除科目 UPDATE_SUBJECT:"UPDATE_SUBJECT", //更新科目 ADD_SCORE:"ADD_SCORE", //添加分数 DELETE_SCORE:"DELETE_SCORE", //删除分数 UPDATE_SCORE:"UPDATE_SCORE" //更新分数 }; export default action_constants;

一个Action就是一个JavaScript对象,它至少包含一个类型字段,如下所示:

//一个Action对象
{ type:"ADD_NAME" }

有的时候,开发者会把Action的type拼写错误,毕竟是字符串。为了解决这个问题,JavaScript的常量就派上了用场。将Action定义为常量可以使得开发者充分利用IDE智能提示和代码补全功能。这个一个预防失误的不错选择。如下所示:

//使用常量后,IDE都会有提示,开发非常方便
import C from './action_constants'

{
    type: C.ADD_NAME
}
{
    type: C.UPDATE_NAME
}

Action是以JavaScript语法的形式提供更新某个State所需的一系列指令的,例如某些数据需要更新,某些数据需要删除,某些数据需要添加等等,这些数据可能是一个对象,也可能是一个字段,这类的数据统称为Action的有效载荷数据。Action是小而美的数据包,能够方便地告知Redux如何修改State。举例如下所示,可以发现第一个删除行为的Action的有效载荷数据不包括name字段。

// 删除姓名的Action对象
// type是必须要的,以便确认什么行为
// name_id也是必须要的,以便确认删除目标
{
    type: C.UPDATE_DELETE,
    name_id:"1"
}


// 添加姓名的Action对象
// type是必须要的,以便确认什么行为
// name_id也是必须要的,新的标识
// name也是必须要的,新的姓名
{
    type: C.ADD_NAME,
    name_id:"3",
    name:"王五"
}

在Redux中,当通过store.dispatch()函数给每种Action派发执行时,都需要开发者去定义一个Action对象,这个过程虽说不难但是也是复杂的。开发者完全可以通过给每种Action类型添加一个Action生成器来简化Action的逻辑。如下所示:

//action_builder.js

import C from './action_constants'

//生成一个”添加姓名“的action
export const addName = (new_name_id, new_name) =>
    ({
        type:C.ADD_NAME,
        name_id:new_name_id
        name:new_name
    });

//生成一个”删除姓名“的action
export const deleteName = (name_id) =>
    ({
        type:C.DELETE_NAME,
        name_id
    });

//生成一个”更新姓名“的action
export const updateName = (name_id, name) =>
    ({
        type:C.UPDATE_NAME,
        name_id,
        name
    });

 

 四、Reducer

虽然现在整个State树都是存储到了单个对象中,但是它的模块化程度还是不高,例如开发者想用模块化的方式描述对象。Redux就是通过函数来实现模块化的,函数被用来更新部分State树中的内容,这些函数被称为Reducer。它会把当前的State和Action当做参数,然后返回一个新的State对象。Reducer的主要目的就是实现State树中部分数据的更新,包括叶子节点和分支。每一个Reducer都是模块化的体现,然后可以将多个Reducer合成一个Reducer,来处理应用程序中任意给定的Action的所有State更新。如图所示:names数组的Reducer处理的Action包括ADDNAME、DELETENAME、UPDATENAME。name对象的Reducer处理的Action包括ADDNAME和UPDATENAME。可以看出,接收的参数为数组,返回的也是数组,接收的参数是对象返回的也是一个对象。

每一个Reducer都可以被合成或者组合成单个的Reducer函数,以便在Store中使用。例如names数组的Reducer是由name对象的Reducer合成的。正是这种合成和单个函数的特点,提供了一种可以更新整个State树和处理任何接收的Action的方式。注意,Reducer合成不是必需的,这是一种推荐做法,我们也可以创建一个Reducer函数来处理所有的Action,但是这样就会失去模块化和函数式编程带来的优势。 用代码表示如下:

//action_reducer.js

import C from './action_constants'

//处理name对象的action的Reducer函数
export const name = (state={}, action) => {
    
    switch (action.type) {
        case C.ADD_NAME:
            return {
                name_id:action.name_id,
                name:action.name
            };
        case C.UPDATE_NAME:
            return (state.name_id !== action.name_id) ? state : {
                ...state,
                name:action.name
            };
        default:
            return state;
    }
};

//处理names数组的action的Reducer函数
export const names = (state=[], action) => {
    
    switch (action.type) {
        case C.ADD_NAME:
            return [
                ...state,
                name({}, action)
            ];
        case C.DELETE_NAME:
            return state.filter(
                name => name.name_id !== action.name_id
            );
        case C.UPDATE_NAME:
            return state.map(
                name => name(name, action)
            );
        default:
            return state;
    }
};

  

、Store

同Flux的工作原理一样,Store负责完成应用程序中State数据的保存和处理所有State更新的地方。不同的是,Flux设计模式中可以支持多个Store共存,每一个Store只专注于特定的数据集。而Redux中只有一个Store,它会把当前的State和Action传递给每一个单独的Reducer函数进而来更新State。在使用Redux之前,需要在项目中安装和配置Redux,步骤如下:

1、npm安装redux相关组件【开发者根据需要安装需要的组件】

// redux: 本地数据存储空间,相当于本地数据库 
// react-redux: 帮助完成数据订阅 
// redux-thunk:帮助实现异步action
npm install --save redux react-redux redux-thunk

// redux-logger: redux的日志中间件
npm install --save-dev redux-logger

 

2、yarn配置package.json包

Redux提供了关于Store的相关API,例如可以通过Redux的createStore函数通过传入参数State或Reducer创建Store,Redux还有一个专门的combineReducers函数来将所有的Reducer构造成单个Reducer。如我们所知,修改用户应用程序State的唯一方式就是通过Store分发Action。Store提供了一个getState函数获取保存到State数据。Store包含一个dispatch函数,可以接收Action作为参数,当用户通过Store分发某个Action时,该Action将会被分配到与之相应的Reducer上,然后State就被更新了。Store还允许使用subscribe函数订阅每次分发完一个Action后被触发的句柄函数,当然这个订阅函数会返回一个退订函数,可以用来退订之前订阅的监听器。相关API如下所示:

//创建Store
export interface StoreCreator {
  <S, A extends Action, Ext, StateExt>(
    reducer: Reducer<S, A>,
    enhancer?: StoreEnhancer<Ext, StateExt>
  ): Store<S & StateExt, A> & Ext
  <S, A extends Action, Ext, StateExt>(
    reducer: Reducer<S, A>,
    preloadedState?: DeepPartial<S>,
    enhancer?: StoreEnhancer<Ext>
  ): Store<S & StateExt, A> & Ext
}
export const createStore: StoreCreator

//combineReducers合成函数
export function combineReducers<S>(
  reducers: ReducersMapObject<S, any>
): Reducer<S>
export function combineReducers<S, A extends Action = AnyAction>(
  reducers: ReducersMapObject<S, A>
): Reducer<S, A>

//获取State数据
getState(): S

//集成自flux中的Dispatch,拥有Dispatch的所有公共函数
dispatch: Dispatch<A>

//订阅函数,返回值是退订函数
subscribe(listener: () => void): Unsubscribe

顺便提一下,Redux不仅提供了combineReducers函数来合成Reducer,还提供了compose函数。 使用compose函数也可以将若干函数合成单个函数。它的功能很健壮,合成的函数执行的顺序是从右往左执行的。举例如下:

import { compose } from 'redux'
//需求:将scores数组的分数转成列表然后使用逗号拼接成字符串并打印

//1、一般写法
const scoreListString = store.getState().scores.map(s => s.score).join(",");
console.log(scoreListString);

//2、使用compose合成单函数
//首先,从State中获取scores
//然后,绑定scores的映射函数map
//接着,生成一个score数组scoreList
//然后,使用逗号拼接字符串scoreListString
//最后,在控制台上打印结果
const print = compose(
    scoreListString => console.log(scoreListString),
    scoreList => scoreList.join(","),
    map => map(s => s.score),
    scores => scores.map.bind(scores),
    state => state.scores
);
print(store.getState());

 

、中间件

Redux还提供了中间件这个概念,它负责Store的分发管道功能。在Redux中,中间件是由在分发某个Action过程中一系列顺序执行的若干高阶函数构成的。这个高阶函数允许用户在某个Action被分发之前或之后,以及State被更新后,插入某些功能。每个中间件函数都是顺序执行的,它们拥有访问Action、dispatch函数,以及下一个next函数的权限。next函数将会触发更新操作。在next函数被调用之前,State就会被更新。网络上介绍的流程图大致如下:

现在,我们就在Store中使用中间件 。首先创建一个storeFactory工厂类,这个类专门用来管理Store的创建过程。然后,通过这个工厂类接着创建一个包含了日志记录的中间件的Store。storeFactory工厂类所在文件包含一个函数,并导出它,该函数用来对创建Store所需的数据进行分组。当需要用到Store时,直接通过这个函数进行创建即可:

//使用工厂类创建store
const store = storeFactory(initData);

在storeFactory类中插入中间件,需要用到redux中的applyMiddleware应用中间件函数。定义和插入logger中间件,如下所示:

//store_factory.js

import { createStore, combineReducers, applyMiddleware } from 'redux'
import { name, names, subject, subjects, score, scores} from "./action_reducer";
import state_data from "./state_data";

//创建日志中间件
const logger = store => next => action => {
    let result;
    console.groupCollapsed("dispatching", action.type);
    console.log('prev state', store.getState());
    console.log('action', action);
    result = next(action);
    console.log('next state', store.getState());
    console.groupEnd();
};

//创建store工厂函数
//0. 设置应用中间件applyMiddleware函数
//1. 插入中间件logger
//2. 合成Reducer函数
const storeFactory = (stateData=state_data) => 
    applyMiddleware(logger)(createStore)(
        combineReducers({name,subject,score,names,subjects,scores}),
        state_data
    )

export default storeFactory

分析结果:在logger中,在该Action被分发之前,打开了一个新的控制台分组console.groupCollapsed,记录了当前的State和Action。触发了next管道上的Action顺序执行中间件函数,最终到达Reducer。此时的State已经更新了,因此我们记录被修改过的State,最后在控制台上分组显示。console.groupEnd()代表中分组结束。

 

、综合应用

下面是完整的代码文件,如下所示: 

1、State数据文件:state_data.js

//存储的数据
const state_data = {
    names:[
        {
            "name_id":"1",
            "name":"张三",
        },
        {
            "name_id":"2",
            "name":"李四",
        }
    ],
    subjects:[
        {
            "subject_id":"1",
            "subject":"数学",
        },
        {
            "subject_id":"2",
            "subject":"语文",
        }
    ],
    scores:[
        {
            "score_id":"1",
            "score": 90,
        },
        {
            "score_id":"2",
            "score": 95,
        }
    ]
};

export default state_data;
View Code

2、Action动词文件:action_constants.js

//定义action类型的动词
const action_constants = {
    ADD_NAME:"ADD_NAME",                //添加姓名
    DELETE_NAME:"DELETE_NAME",          //删除姓名
    UPDATE_NAME:"UPDATE_NAME",          //更新姓名
    ADD_SUBJECT:"ADD_SUBJECT",          //添加科目
    DELETE_SUBJECT:"DELETE_SUBJECT",    //删除科目
    UPDATE_SUBJECT:"UPDATE_SUBJECT",    //更新科目
    ADD_SCORE:"ADD_SCORE",              //添加分数
    DELETE_SCORE:"DELETE_SCORE",        //删除分数
    UPDATE_SCORE:"UPDATE_SCORE"         //更新分数
};

export default action_constants;
View Code

3、Action生成器文件:action_builder.js

import C from './action_constants'

//生成一个"添加姓名"的action
export const addName = (new_name_id, new_name) =>
    ({
        type:C.ADD_NAME,
        name_id:new_name_id,
        name:new_name
    });

//生成一个"删除姓名"的action
export const deleteName = (name_id) =>
    ({
        type:C.DELETE_NAME,
        name_id
    });

//生成一个"更新姓名"的action
export const updateName = (name_id, name) =>
    ({
        type:C.UPDATE_NAME,
        name_id,
        name
    });

//生成一个"添加科目"的action
export const addSubject = (new_subject_id, new_subject) =>
    ({
        type:C.ADD_SUBJECT,
        subject_id:new_subject_id,
        subject:new_subject
    });

//生成一个"删除科目"的action
export const deleteSubject = (subject_id) =>
    ({
        type:C.DELETE_SUBJECT,
        subject_id
    });

//生成一个"更新科目"的action
export const updateSubject = (subject_id, subject) =>
    ({
        type:C.UPDATE_SUBJECT,
        subject_id,
        subject
    });

//生成一个"添加分数"的action
export const addScore = (new_score_id, new_score) =>
    ({
        type:C.ADD_SCORE,
        score_id:new_score_id,
        score:new_score
    });

//生成一个"删除分数"的action
export const deleteScore = (score_id) =>
    ({
        type:C.DELETE_SCORE,
        score_id
    });

//生成一个"更新分数"的action
export const updateScore = (score_id, score) =>
    ({
        type:C.UPDATE_SCORE,
        score_id,
        score
    });
View Code 

4、Reducer函数文件:action_reducer.js

import C from './action_constants'

//处理name对象的action的Reducer函数
export const name = (state={}, action) => {

    switch (action.type) {
        case C.ADD_NAME:
            return {
                name_id:action.name_id,
                name:action.name
            };
        case C.UPDATE_NAME:
            return (state.name_id !== action.name_id) ? state : {
                ...state,
                name:action.name
            };
        default:
            return state;
    }
};


//处理names数组的action的Reducer函数
export const names = (state=[], action) => {

    switch (action.type) {
        case C.ADD_NAME:
            return [
                ...state,
                name({}, action)
            ];
        case C.DELETE_NAME:
            return state.filter(
                name => name.name_id !== action.name_id
            );
        case C.UPDATE_NAME:
            return state.map(
                nameObj => name(nameObj, action)
            );
        default:
            return state;
    }
};

//处理subject对象的action的Reducer函数
export const subject = (state={}, action) => {

    switch (action.type) {
        case C.ADD_SUBJECT:
            return {
                subject_id:action.subject_id,
                subject:action.subject
            };
        case C.UPDATE_SUBJECT:
            return (state.subject_id !== action.subject_id) ? state : {
                ...state,
                subject:action.subject
            };
        default:
            return state;
    }
};

//处理subjects数组的action的Reducer函数
export const subjects = (state=[], action) => {

    switch (action.type) {
        case C.ADD_SUBJECT:
            return [
                ...state,
                subject({}, action)
            ];
        case C.DELETE_SUBJECT:
            return state.filter(
                subject => subject.subject_id !== action.subject_id
            );
        case C.UPDATE_SUBJECT:
            return state.map(
                subjectObj => subject(subjectObj, action)
            );
        default:
            return state;
    }
};

//处理score对象的action的Reducer函数
export const score = (state={}, action) => {

    switch (action.type) {
        case C.ADD_SCORE:
            return {
                score_id:action.score_id,
                score:action.score
            };
        case C.UPDATE_SCORE:
            return (state.score_id !== action.score_id) ? state : {
                ...state,
                score:action.score
            };
        default:
            return state;
    }
};

//处理scores数组的action的Reducer函数
export const scores = (state=[], action) => {

    switch (action.type) {
        case C.ADD_SCORE:
            return [
                ...state,
                score({}, action)
            ];
        case C.DELETE_SCORE:
            return state.filter(
                score => score.score_id !== action.score_id
            );
        case C.UPDATE_SCORE:
            return state.map(
                scoreObj => score(scoreObj, action)
            );
        default:
            return state;
    }
};
View Code 

5、Store工厂类文件:store_factory.js

import { createStore, combineReducers, applyMiddleware } from 'redux'
import { name, names, subject, subjects, score, scores} from "./action_reducer";
import state_data from "./state_data";

//创建日志中间件
const logger = store => next => action => {
    console.groupCollapsed("dispatching", action.type);
    console.log('prev state:', store.getState());
    console.log('action:', action);
    next(action);
    console.log('next state:', store.getState());
    console.groupEnd();
};

//创建store工厂函数
//0. 设置应用中间件applyMiddleware函数
//1. 插入中间件logger
//2. 合成Reducer函数
const storeFactory = (stateData=state_data) =>
    applyMiddleware(logger)(createStore)(
        combineReducers({name, names, subject, subjects, score, scores}),
        state_data
    )

export default storeFactory;
View Code

 

下面是测试代码,如下所示:

1、获取state数据

//获取state数据
import state_data from "./redux/state_data";
console.log(
"1————state_data:",state_data);

2、手动创建action,action中至少有一个type字段用于区分类别

//手动创建action,action中至少有一个type字段用于区分类别
import C from './redux/action_constants'

const manually_addName_Action = { type: C.ADD_NAME, name_id:"3", name:"王五"}; const manually_deleteName_Action = { type: C.DELETE_NAME, name_id:"2"}; const manually_updateName_Action = { type: C.UPDATE_NAME, name_id:"1", name:"赵六"}; console.log("manually_addName_Action:", manually_addName_Action); console.log("manually_deleteName_Action:", manually_deleteName_Action); console.log("manually_updateName_Action:", manually_updateName_Action); const manually_addSubject_Action = { type: C.ADD_SUBJECT, subject_id:"3", subject:"地理"}; const manually_deleteSubject_Action = { type: C.DELETE_SUBJECT, subject_id:"2"}; const manually_updateSubject_Action = { type: C.UPDATE_SUBJECT, subject_id:"1", subject:"政治"}; console.log("manually_addSubject_Action:", manually_addSubject_Action); console.log("manually_deleteSubject_Action:", manually_deleteSubject_Action); console.log("manually_updateSubject_Action:", manually_updateSubject_Action); const manually_addScore_Action = { type: C.ADD_SCORE, score_id:"3", score:50}; const manually_deleteScore_Action = { type: C.DELETE_SCORE, score_id:"2"}; const manually_updateScore_Action = { type: C.UPDATE_SCORE, score_id:"1", score:100}; console.log("manually_addScore_Action:", manually_addScore_Action); console.log("manually_deleteScore_Action:", manually_deleteScore_Action); console.log("manually_updateScore_Action:", manually_updateScore_Action);

3、使用生成器创建action

//使用生成器创建action
import {addName, deleteName, updateName, 
        addSubject, deleteSubject, updateSubject, 
        addScore, deleteScore, updateScore} from "./redux/action_builder";

const auto_addName_Action = addName("3","王五");
const auto_deleteName_Action = deleteName("2");
const auto_updateName_Action = updateName("1","赵六");
console.log("auto_addName_Action:", auto_addName_Action);
console.log("auto_deleteName_Action:", auto_deleteName_Action);
console.log("auto_updateName_Action:", auto_updateName_Action);

const auto_addSubject_Action = addSubject("3","地理");
const auto_deleteSubject_Action = deleteSubject("3");
const auto_updateSubject_Action = updateSubject("1","政治");
console.log("auto_addSubject_Action:", auto_addSubject_Action);
console.log("auto_deleteSubject_Action:", auto_deleteSubject_Action);
console.log("auto_updateSubject_Action:", auto_updateSubject_Action);

const auto_addScore_Action =  addScore("3", 50);
const auto_deleteScore_Action = deleteScore("3");
const auto_updateScore_Action = updateScore("1", 100);
console.log("auto_addScore_Action:", auto_addScore_Action);
console.log("auto_deleteScore_Action:", auto_deleteScore_Action);
console.log("auto_updateScore_Action:", auto_updateScore_Action);

4、Reducer函数,模块化

//打印对象的Reducer和数组的Reducer
import { name, subject, score, names, subjects, scores } from "./redux/action_reducer";

console.log("name_reducer:",name);
console.log("subject_reducer:",subject);
console.log("score_reducer:",score);
console.log("names_reducer:",names);
console.log("subjects_reducer:",subjects);
console.log("scores_reducer:",scores);

 

5、合成多个Reducer成单个Reducer

// 合成多个Reducer为单个Reducer
import {combineReducers} from "redux";

const combineReducer = combineReducers(
    { name, subject, score, names, subjects, scores});
console.log("combineReducer:",combineReducer);

6、定义store,分派action,观察state数据的变化

// 定义store,分派action
import {createStore} from "redux";

const store = createStore(combineReducer, state_data);
console.log("store:",store);

store.dispatch(auto_addName_Action);
console.log("2————state_data:",store.getState());

7、订阅Store,添加logger句柄函数,打印日志

//订阅store,添加一个打印subjects的句柄函数
const logger = () => console.log('subscribe-----subjects:',store.getState().subjects);
const unsubscribeLogger = store.subscribe(logger);

//分发store,触发句柄函数
store.dispatch(auto_addSubject_Action);
store.dispatch(auto_deleteSubject_Action);
store.dispatch(auto_updateSubject_Action);

//退订监听器
unsubscribeLogger();

8、插入中间件,在控制台分组中观察state数据变化以及logger打印插件的结果

//通过类方法创建store
import storeFactory from "./redux/store_factory";

const store = storeFactory();
console.log("store:", store);

//派发Action
store.dispatch(auto_addName_Action);
console.log("3————state_data:",store.getState());

store.dispatch(auto_deleteName_Action);
console.log("4————state_data:",store.getState());

store.dispatch(auto_updateName_Action);
console.log("5————state_data:",store.getState());

 

posted @ 2019-12-22 00:01  XYQ全哥  阅读(296)  评论(0编辑  收藏  举报