一. reducer拆分
1.1. reducer代码拆分
我们来看一下目前我们的reducer:
- 当前这个reducer既有处理counter的代码,又有处理home页面的数据;
- 后续counter相关的状态或home相关的状态会进一步变得更加复杂;
- 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
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中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
因此,我们可以对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编码过程中,数据的不可变性都是非常重要的;
数据的可变性引发的问题:
- 我们明明没有修改obj,只是修改了obj2,但是最终obj也被我们修改掉了;
- 原因非常简单,对象是引用类型,它们指向同一块内存空间,两个引用都可以任意修改;
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}
有没有办法解决上面的问题呢?
- 进行对象的拷贝即可: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(持久化数据结构或一致性数据结构);
当然,我们一听到持久化第一反应应该是数据被保存到本地或者数据库,但是这里并不是这个含义:
- 用一种数据结构来保存数据;
- 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费;
如何做到这一点呢?结构共享:
- 如果在公众号等地方动图不能正常显示,可以查看下面的静态图;
Immutable 原理动画 Immutable data
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来维护;
当然,根据不同的情况会进行适当的调整,在后续学习网易云音乐项目时,我也会再次讲解以实战的角度来设计数据的管理方案。