createStore与combineReducers
Redux思想
Redux是一个Javascript状态容器,提供可预测化的状态管理。对于一个复杂点的应用来说,管理其不断变化的State是很困难的,如果不对状态的修改加以限制,状态之间互相关联影响;那样我们就无法预料一个操作的触发,会带来什么样的变化。
单一数据源
整个应用的state
存在于全局唯一的一个Store
上,这个唯一的Store
是一个树形的对象,里面包含了应用中的所有状态,每个组件往往都是用树形对象上一部分的数据。
保持状态只读
这里说的保持状态只读,意思是不能直接去修改Store
中的状态,要想修改应用的state
必须通过dispatch
派发一个action
对象来完成。
数据的修改使用纯函数完成。
这里的纯函数就是reducer(state, action)
,第一个参数state
是当前的状态,第二个参数action
是用于dispatch
的对象,函数最后返回的新state
值完全是根据传入的参数值来确定的。
createStore
Redux
库对外暴露的属性不多, createStore
就是其中最重要的一个,用于生成我们应用的store
。store
为用户主要提供以下属性
//删除了enhancer部分源码的createStore
function createStore(reducer, preloadedState) {
var currentReducer = reducer;//reducer函数
var currentState = preloadedState;//默认state
var currentListeners = []; //当前的监听函数
var nextListeners = currentListeners;
var isDispatching = false; //应用当前是否正在执行派发操作
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
//数组浅拷贝, 生成一个新的数组, 和以前的数组已经没有关系了, 至于这样做的目的后面会说到
nextListeners = currentListeners.slice();
}
}
function getState() {}
function dispatch(action) {}
function subscribe(listener) {}
//用于生成应用默认的state
dispatch({ type: ActionTypes.INIT });
}
-
getState()
通过闭包技术,获取应用状态function getState() { if (isDispatching) {...}// 在reducer执行的过程中不可以获取state return currentState; //通过闭包技术,在函数外部获取函数函数内定义的局部变量currentState }
-
dispatch()
接收一个必须含有type
属性的action
对象作为参数,用于派发事件,并调用reducer
函数,返回一个新的state
function dispatch(action) { //action必须是一个对象 if (!isPlainObject(action)) { ... } //action对象必须包含type属性 if (typeof action.type === 'undefined') { ... } //当前不能有正在执行的dispatch操作 if (isDispatching) { ... } try { isDispatching = true; //调用reducer返回新的state,其中新state值完全取决于传入的参数currentState、action,所以需要reducer是一个纯函数,才能够使状态的变化可预测、有因可循。 currentState = currentReducer(currentState, action); } finally { isDispatching = false; //reducer函数执行结束,改变状态 } //createStore源码中有两处currentListeners和nextListeners互相赋值, 另外一处在createStore函数最开始执行时;明明一个变量就能解决问题, 至于为啥源码中要用两个变量来维护listeners,后面会专门解释 var listeners = currentListeners = nextListeners; //遍历监听函数,并依次执行,通知监听的页面状态改变 for (var i = 0; i < listeners.length; i++) { var listener = listeners[i]; listener(); } return action; }
-
subscribe()
订阅函数, 接收一个回调函数作为参数,用于通知组件状态更新了function subscribe(listener) { if (typeof listener !== 'function') { throw new Error() }// 传入的listener必须是函数 if (isDispatching) { throw new Error() } //当前不能有正在执行的dispatch操作 var isSubscribed = true; //保存一份当前的listeners快照,此时currentListeners与currentListeners已经彼此独立,二者指向不同的引用。 ensureCanMutateNextListeners(); nextListeners.push(listener); //注意此时nextListeners比currentlisteners新,后者还是未push之前的 return function unsubscribe() { //返回一个函数用于取消当前的订阅 if (!isSubscribed) { return;} if (isDispatching) { throw new Error() } isSubscribed = false; ensureCanMutateNextListeners();//保存一份当前的listeners快照 var index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); //在监听数组nextListeners中删除当前监听的函数, currentListeners = null; }; }
-
unsubscribe()
取消订阅事件订阅函数的返回值是一个函数, 用于取消当前的订阅,直接调用该函数就可以取消订阅
回调上面遗留的问题,监听函数数组listeners
,明明用一个变量就可以维护,为啥源码要用两组变量currentListeners
、nextListeners
来维护??在订阅事件和取消订阅事件中都调用了ensureCanMutateNextListeners
函数有啥用??
//如果订阅事件内部还有一个订阅事件
store.subscribe(function() {
console.log(1)
//在当前这个回调函数执行时,才会执行下面的代码
store.subscribe(function() {
console.log(2)
})
})
//如果我们模拟执行dispatch, 在订阅事件中不调用`ensureCanMutateNextListeners`函数时, 那么打印的结果会是什么呢?
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
listener();
}
// 此时1, 2都会打印
//调用`ensureCanMutateNextListeners`函数时, 只会打印 1
redux
的主要目的就是将在dispatch
执行过程中产生的订阅和退订事件,不会在当前立即体现, 但是会在下一个dispatch
时才体现,所以redux为此做了两件事:
1.每次在执行订阅或退订事件之前生成一份当前的listeners
副本,将currentListeners
、nextListeners
分别指向不同的引用,后续监听函数的增删只对nextListeners
有效,currentListeners
保持不变
2.每次在执行dispatch
时,会在reducer
函数执行完之后将currentListeners
、nextListeners
指向同一个引用;
结论的得出看似一切都这么顺风顺水,但是我在最开始实验时,遍历执行监听函数用的不是for循环,而是直接用数组的map
方法,结果却出人意料,也只打印了1, 即当前的变动没有立即体现。
listeners.map( listener => listener() ) // 只打印了 1
实验一下,如果数组在使用map
遍历的过程中,原数组发生了变化,遍历的结果是否会受到影响呢?
var arr1 = [1,2,3,4,5]
var arr2 = []
var arr3 = arr1.map(item => {
if(item ===1) {
arr1.push(100)
}
return item
})
arr3 // [1, 2, 3, 4, 5] 当前没变化
var arr4 = arr1.map(item => {
if(item ===1) {
arr1.push(100)
}
return item
})
arr4 // [1, 2, 3, 4, 5, 100] 第二次遍历才有变化
不仅是map
,forEach
也是这样, 但是我在网上也没有找到原因, 我猜想是不是这些这些方法在封装时,也做了类似redux
一样的处理,(当前的变化不立即体现,下一次触发才会体现);
combineReducers
整个应用的state
是一个object tree
, 里面包含了应用中的所有状态,每个组件中往往都是取之这个大object tree
上一部分的数据。而且随着应用越变越复杂, 就需要对庞大的reducer
函数进行拆分,让每个子reduce
r只负责处理object tree
中的一部分数据。这样一来,我们就需要一个主函数将这些子reducer
生成的子state
,组合成一个完整的object tree
且结构需要与之前的保持一致。
所以combineReducers
要做的事情就是要返回一个函数,返回的这个函数就是我们传传递给createStore
的第一个参数 reducer
函数,而且返回的这个函数需要将子reduce
r生成的子state
整合并返回。说的有点绕呀~,直接进源码吧
function combineReducers(reducers) {//注意这里的reducers是由一系列子reducer作为键值构成的对象
var reducerKeys = Object.keys(reducers); //获取对象的键名
var finalReducers = {}; //浅拷贝最终所有满足条件的reducer
for (var i = 0; i < reducerKeys.length; i++) {
var key = reducerKeys[i];
if (typeof reducers[key] === 'function') { //reducer必须是函数,
finalReducers[key] = reducers[key]; //函数浅拷贝
}
}
var finalReducerKeys = Object.keys(finalReducers); //获取最终的reducer键名
return function combination(state, action) { //接收一个state和action?? 这不就是reducer函数么
//void 0 等同于 undefined, 用void 0 替代 undefined的好处有两个, 在局部作用域中undefined可能会被当成变量重写; 相比undefined9个字符void 0更少呀;
if (state === void 0) {
state = {};
}
var hasChanged = false; //用于判断状态是否改变
var nextState = {};
//
for (var _i = 0; _i < finalReducerKeys.length; _i++) {
var _key = finalReducerKeys[_i]; //获取kek
var reducer = finalReducers[_key]; //根据key获取对应的子reducer
var previousStateForKey = state[_key]; //根据key获取此时对应的子state
var nextStateForKey = reducer(previousStateForKey, action);//调用reducer函数,返回新的state
nextState[_key] = nextStateForKey; //根据reducerKey组合State
//浅比较判断当前子state是否变化, 遍历过程中只要有一次hasChanged为true,那么最终hasChanged就为true
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
//假如默认的state是一个空对象{},而且经子reducer计算返回的值也是undefined时,上面的hasChanged就判断不了啦, 因为nextStateForKey 和 previousStateForKey都是undefined。所以下面这行代码是针对那些原state中没有定义的属性, 但经reducer计算处理后依然返回undefined的情况
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
};
}