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;
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;
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 });
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; } };
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;
下面是测试代码,如下所示:
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());