本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
一. reducer拆分
1.1. reducer代码拆分
我们来看一下目前我们的reducer
:
function reducer(state = initialState, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
case SUB_NUMBER:
return { ...state, counter: state.counter - action.num };
case CHANGE_BANNER:
return { ...state, banners: action.banners };
case CHANGE_RECOMMEND:
return { ...state, recommends: action.recommends };
default:
return state;
}
}
- 当前这个
reducer
既有处理counter
的代码,又有处理home
页面的数据; - 后续
counter
相关的状态或home
相关的状态会进一步变得更加复杂; - 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
如果将所有的状态都放到一个reducer
中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
因此,我们可以对reducer
进行拆分:
我们先抽取一个对counter
处理的reducer
:
// counter相关的状态
const initialCounter = {
counter: 0
}
function counterReducer(state = initialCounter, action) {
switch (action.type) {
case ADD_NUMBER:
return { ...state, counter: state.counter + action.num };
case SUB_NUMBER:
return { ...state, counter: state.counter - action.num };
default:
return state;
}
}
再抽取一个对home
处理的reducer
:
// home相关的状态
const initialHome = {
banners: [],
recommends: []
}
function homeReducer(state = initialHome, action) {
switch (action.type) {
case CHANGE_BANNER:
return { ...state, banners: action.banners };
case CHANGE_RECOMMEND:
return { ...state, recommends: action.recommends };
default:
return state;
}
}
如果将它们合并起来呢?
const initialState = {
}
function reducer(state = initialState, action) {
return {
counterInfo: counterReducer(state.counterInfo, action),
homeInfo: homeReducer(state.homeInfo, action),
}
}
1.2. reducer文件拆分
目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:
- 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
- 另外关于
reducer
中用到的constant、action
等我们也依然是在同一个文件中;
所以,接下来,我们可以对文件结构再次进行拆分:
./store
├── counter
│ ├── actioncreators.js
│ ├── constants.js
│ ├── index.js
│ └── reducer.js
├── home
│ ├── actioncreators.js
│ ├── constants.js
│ ├── index.js
│ └── reducer.js
├── index.js
├── reducer.js
└── saga.js
这里不再给出代码,每个文件中存放的内容即可:
home/actioncreators.js
:存放home
相关的action
;home/constants.js
:存放home
相关的常量;home/reducer.js
:存放分离的reducer
代码;index.js
:统一对外暴露的内容;
1.3. combineReducers
目前我们合并的方式是通过每次调用reducer
函数自己来返回一个新的对象:
import { reducer as counterReducer } from './counter';
import { reducer as homeReducer } from './home';
const initialState = {
}
function reducer(state = initialState, action) {
return {
counterInfo: counterReducer(state.counterInfo, action),
homeInfo: homeReducer(state.homeInfo, action),
}
}
事实上,redux
给我们提供了一个combineReducers
函数可以方便的让我们对多个reducer
进行合并:
import { combineReducers } from 'redux';
import { reducer as counterReducer } from './counter';
import { reducer as homeReducer } from './home';
const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
})
export default reducer;
那么combineReducers
是如何实现的呢?
- 事实上,它也是讲我们传入的
reducers
合并到一个对象中,最终返回一个combination
的函数(相当于我们之前的reducer
函数了); - 在执行
combination
函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state
还是新的state
; - 新的
state
会触发订阅者发生对应的刷新,而旧的state
可以有效的组织订阅者发生刷新;
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action)
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state
}
}
二. ImmutableJS
2.1. 数据可变性的问题
在React开发中,我们总是会强调数据的不可变性:
- 无论是类组件中的
state
,还是redux
中管理的state
; - 事实上在整个JavaScript编码过程中,数据的不可变性都是非常重要的;
数据的可变性引发的问题:
const obj = {
name: "why",
age: 18
}
console.log(obj); // {name: "why", age: 18}
const obj2 = obj;
obj2.name = "kobe";
console.log(obj); // {name: "kobe", age: 18}
- 我们明明没有修改
obj
,只是修改了obj2
,但是最终obj
也被我们修改掉了; - 原因非常简单,对象是引用类型,它们指向同一块内存空间,两个引用都可以任意修改;
有没有办法解决上面的问题呢?
- 进行对象的拷贝即可:
Object.assign
或扩展运算符
console.log(obj); // {name: "why", age: 18}
const obj2 = {...obj};
obj2.name = "kobe";
console.log(obj); // {name: "kobe", age: 18}
这种对象的浅拷贝有没有问题呢?
- 从代码的角度来说,没有问题,也解决了我们实际开发中一些潜在风险;
- 从性能的角度来说,有问题,如果对象过于庞大,这种拷贝的方式会带来性能问题以及内存浪费;
有人会说,开发中不都是这样做的吗?
- 我比较喜欢一句话:从来如此,便是对的吗?
2.2. 认识ImmutableJS
为了解决上面的问题,出现了Immutable
对象的概念:
-
Immutable
对象的特点是只要修改了对象,就会返回一个新的对象,旧的对象不会发生改变;
但是这样的方式就不会浪费内存了吗? -
为了节约内存,又出现了一个新的算法:
Persistent Data Structure
(持久化数据结构或一致性数据结构);
当然,我们一听到持久化第一反应应该是数据被保存到本地或者数据库,但是这里并不是这个含义:
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;
如何做到这一点呢?结构共享:
2.3. ImutableJS常见API
我们来学习一下ImmutableJS
常用的API:
- 注意:我这里只是演示了一些API,更多的方式可以参考官网;
const imjs = Immutable;
// 1.定义JavaScript的Array和转换成immutable的List
const friends = [
{ name: "why", age: 18 },
{ name: "kobe", age: 30 }
]
// 不会进行深层转换
const imArray1 = imjs.List(friends);
// 会进行深层转换
const imArray2 = imjs.fromJS(friends);
// console.log(imArray1);
// console.log(imArray2);
// 1.定义JavaScript的Object和转换成immutable的Map
const info = {
name: "coderwhy",
age: 18,
friend: {
name: "kobe",
age: 30
}
}
const imObj1 = imjs.Map(info);
const imObj2 = imjs.fromJS(info);
// console.log(imObj1);
// console.log(imObj2);
// 3.对immutable操作
// 3.1.添加数据
// 产生一个新的immutable对象
console.log(imArray1.push("aaaa"));
console.log(imArray1.set(2, "aaaa"));
// 原来的是不变的
console.log(imArray1);
// 3.2.修改数据
console.log(imArray1.set(1, "aaaa"));
console.log(imArray2.set(2, imjs.fromJS("bbbb")));
// 3.3.删除数据
console.log(imArray1.delete(0).get(0)); // {name: "kobe", age: 30}
// 3.4.查询数据
console.log(imArray1.get(1));
console.log(imArray2.get(1));
console.log(imArray1.get(1).name);
console.log(imArray2.getIn([1, "name"]));
// 4.转换成JavaScript对象
const array = imArray1.toJS();
const obj = imObj1.toJS();
console.log(array);
console.log(obj);
2.4. ImmutableJS重构redux
目前Redux已经学习了过多的内容了,很多同学已经认识到redux的难度,所以如何将ImmutableJS和redux结合使用我们这里先不讲解。
具体ImmutableJS和Redux的结合使用,放到后续项目练习时,再详细说明。
三. Redux FAQ
是否将所有的状态应用到redux
我们学习了Redux用来管理我们的应用状态,并且非常好用(当然,你学会前提下,没有学会,好好回顾一下)。
目前我们已经主要学习了三种状态管理方式:
- 方式一:组件中自己的
state
管理; - 方式二:
Context
数据的共享状态; - 方式三:
Redux
管理应用状态;
在开发中如何选择呢?
- 首先,这个没有一个标准的答案;
- 某些用户,选择将所有的状态放到
redux
中进行管理,因为这样方便追踪和共享; - 有些用户,选择将某些组件自己的状态放到组件内部进行管理;
- 有些用户,将类似于主题、用户信息等数据放到
Context
中进行共享和管理; - 做一个开发者,到底选择怎样的状态管理方式,是你的工作之一,可以一个最好的平衡方式(Find a balance that works for you, and go with it.);
redux作者建议:
目前项目中我采用的state
管理方案:
- UI相关的组件内部可以维护的状态,在组件内部自己来维护;
- 只要是需要共享的状态,都交给redux来管理和维护;
- 从服务器请求的数据(包括请求的操作),交给redux来维护;
当然,根据不同的情况会进行适当的调整,在后续学习网易云音乐项目时,我也会再次讲解以实战的角度来设计数据的管理方案。