如何实现 Redux
示例 demo:github
注意:为了简单明了的展示主要流程,下面示例中均会省略异常情况的处理。
redux
里有四个核心的 api,下面会按顺序介绍并实现它们
- combineReducer
- compose
- createStore
- applyMiddlewares
一、combineReducer
1. combineReducer 是什么,有什么用?
因为createStore
只能接收一个reducer
,如果把所有的reducer
都写在一起的话会很臃肿,所以需要将其分割为一个个小的reducer
,分别负责一部分的 state,再合并成一个rootReducer
。
combineReducer
的作用就是将多个reducer
合并成一个
合并后返回的 reducer 在处理 state 时会进行分发,从上面接收 state 后会将对应部分的 state 分发给 reducer
不理解的话,可以看下边的执行前后代码
2. combineReducer 执行前后代码
export default combineReducer({
list: listReducers,
dict: dictReducers,
detail: detailReducers,
});
//↓↓↓等价于↓↓↓
export default function (state, action) {
return {
list: listReducers(state.list, action),
dict: dictReducers(state.dict, action),
detail: detailReducers(state.detail, action),
};
}
3. 实现 combineReducer
遍历传入的对象拿到所有的 key 值,使用 key 值拿到对应的旧状态,将旧状态和动作传入reducer
,获取最新状态并返回,实现起来并不复杂。
function combineReducer(reducers) {
return function (state, action) {
const nextState = {};
for (let key in reducers) {
nextState[key] = reducers[key](state[key], action);
}
return nextState;
};
}
4. redux 是如何实现 combineReducer?
对比下源码
function combineReducer(reducers) {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === "function") {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
return function (state = {}, action) {
let hasChange = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[i];
const prevStateForKey = state[key];
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
hasChange = hasChange || nextStateForKey !== prevStateForKey;
}
return hasChange ? nextState : state;
};
}
需要注意的点:
- 除函数以外的值会被过滤,只处理对象里的函数。
- 在返回函数里会比较前后状态,如果未变化则返回旧状态
二、compose
1. compose 有什么作用?
compose
主要用于中间件的整合,接收多个函数传参,返回单个函数。当执行返回函数时,相当于嵌套执行传参的函数。
说起来比较绕,可以看下边的例子理解
compose(a, b, c); // a b c Function
//↓↓↓等价于↓↓↓
function Fn(...args) {
return a(b(c(...args)));
}
2. 如何实现?
思路很简单,可以用循环遍历,返回嵌套函数,也可以用reduce
方法
//第一种
function compose(...fns) {
if (fns.length === 0) {
return;
}
let componseFn = (...args) => fns[0](...args);
for (let i = 1; i < fns.length; i++) {
let prevFn = componseFn;
componseFn = (...args) => prevFn(fns[i](...args));
}
return componseFn;
}
//第二种
function compose(...fns) {
if (fns.length === 0) {
return (arg) => arg;
}
if (fns.length === 1) {
return fns[0];
}
return fns.reduce((a, b) => {
return (...args) => a(b(...args));
});
}
三、createStore
createStore
最多可以接收三个传参,分别是reducer, preloadedState, enhancer
reducer
为必传项,preloadedState
和enhancer
为非必传,当只传递两个参数时,createStore
会检查第二个参数的类型,如何是 function 类型的话按enhancer
处理,其他情况会当作preloadedState
reducer
:响应 action,更新 statepreloadState
:初始化 stateenhancer
:applyMiddlewares
返回的函数,用于增强dispatch
的功能
reducer
方法传参里提供初始值也可以达到初始化 state 的效果,但和使用preloadState
存在一些区别
preloadState
初始化 state 在createStore
执行时生效,而函数传参初始值必须要在触发动作并执行 reducer 函数后才会生效,但事实上,createStore
在返回结果前会主动触发一个初始化动作(指 action),所以这两者都能达到初始化 state 的效果。实际开发中初始化数据放在 reducer 里多一些,分割初始状态的同时也能直观看到对应的状态
createStore
返回{dispatch<Function>, getState<Function>, subscribe<Function>}
1. 定义 createStore 的传参和输出
function createStore(reducer, enhancer) {
let currentState,
listeners = [];
function dispatch() {}
function subscribe() {}
function getState() {}
return {
dispatch,
subscribe,
getState,
};
}
确认dispatch
、getState
、subscribe
的作用
dispatch
:触发一个动作,修改 state,并触发所有的监听函数getState
:返回当前的 statesubscribe
:订阅监听函数,返回取消订阅方法
2. 补全方法
function createStore(reducer, enhancer) {
let currentState,
listeners = [];
function dispatch(action) {
currentState = reducer(currentState, action);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
}
function subscribe(handleStoreChange) {
listeners.push(handleStoreChange);
return function unsubscribe() {
const index = listeners.indexOf(handleStoreChange);
listeners.splice(index, 1);
};
}
function getState() {
return currentState;
}
dispatch({ type: "INIT" }); // 初始化
return {
dispatch,
subscribe,
getState,
};
}
四、applyMiddleware
1. 中间件
传入的中间件必须是规定的柯里化函数({ getState, dispatch }) => next => action
。
next 为下一个中间件,只有当最后一个中间件时才会是 dispatch
可以简单理解为:
({ getState, dispatch }) =>
(next) =>
action;
//↓↓↓等价于↓↓↓
function middleware({ getState, dispatch }) {
return function (next) {
return function (action) {
// ...do something
return next(action);
};
};
}
执行中间件需要提前传递getState、dispatch
,以及绑定 next
这部分代码不多,但复杂程度却是最高的,建议提前看下源码或是了解下洋葱模型。
2. 源码(简化版)
function createStore(reducer, enhancer) {
...
if (typeof enhancer === "function") {
return enhancer(createStore)(reducer);
}
...
}
function applyMiddleware(...middlewares) {
return function(createStore) {
return function(reducer) {
const store = createStore(reducer);
let chain = [];
let dispatch = store.dispatch;
const middlewareAPI = {
dispatch: action => dispatch(action),
getState: store.getState
}
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
};
};
}
当createStore
传入中间件时,会把自己传给enhancer
,在里面进行store
的创建,以及dispatch
的绑定。
其中最重要最复杂的绑定部分代码,只有两条。
chain = middlewares.map((middleware) => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
- 第一:使用
map
为每个中间绑定dispatch、getState
- 第二:通过
compose
合并成一个函数,传递dispatch
绑定中间件里的 next,得到新的dispatch
。
第一句很好理解,第二句也不难,compose
上面有讲解,最后剩下一点,那就是 next 的绑定。
3. 绑定 next
中间件定义伪代码。
function middlewareA({ dispatch, getState }) {
return function (next) {
return function (action) {
console.log("Middleware A");
return next(action);
};
};
}
function middlewareB({ dispatch, getState }) {
return function (next) {
return function (action) {
console.log("Middleware B");
next(action);
};
};
}
// middlewares = [middlewareA, middlewareB]
// middlewares.map((middleware) => middleware(middlewareAPI));
function middlewareAAfterMap(next) {
//闭包里保持着{getState, dispatch}
return function (action) {
console.log("Middleware A do something");
return next(action);
console.log("Middleware A finish");
};
}
function middlewareBAfterMap(next) {
//闭包里保持着{getState, dispatch}
return function (action) {
console.log("Middleware B do something");
next(action);
console.log("Middleware B finish");
};
}
// chain = [middlewareAAfterMap, middlewareBAfterMap]
// composedFn = compose(...chain);
function composedFn(...args) {
return middlewareAAfterMap(middlewareBAfterMap(...args));
}
// newDispatch = composedFn(dispatch);
// newDispatch = middlewareAAfterMap(middlewareBAfterMap(dispatch))
function newDispatch(action) {
return middlewareAAfterMap(function (action) {
console.log("Middleware B do something");
dispatch(action);
console.log("Middleware B finish");
});
}
↓↓↓
function newDispatch(action) {
return function (action) {
console.log("Middleware A do something");
console.log("Middleware B do something");
dispatch(action);
console.log("Middleware B finish");
console.log("Middleware A finish");
};
}
4. 绑定顺序和执行顺序
applyMiddleware
可以接收多个中间件传参,并从右到左依次绑定中间件,但当dispatch
时,中间件的执行顺序却是从左到右(这也是为什么redux-logger
要放在最后面)。
为什么执行顺序是相反的,可以通过下面的模型理解
———————————
| dispatch |
———————————
↓↓↓绑定中间件↓↓↓
-------------------
| redux-thunk |
| --------------- |
| | redux-logger | |
| | ——————————— | |
| | | dispatch | | |
| | ——————————— | |
| --------------- |
-------------------
绑定中间件相当于在dispatch
外包一层又一层的裹上,而当执行中间件时则需要从外到内顺序执行,所以执行顺序和绑定顺序是相反的
5. 实现
光看还不够,只有实际动手后才会明白里面的细节和关键,最后是我的实现。
function applyMiddleware(...middlewares) {
return function({ getState, dispatch }) {
return function(next) {
return compose(
...middlewares.map(middleware => {
return middleware({ getState, dispatch });
})
)(next);
};
};
}
function createStore(reducer, enhancer) {
let dispatch = function(action) {
...
};
const subscribe = function(handleStoreChange) {
...
};
const getState = function() {
...
};
let currentState = {};
let listeners = [];
let dispatchWithMiddleware = dispatch;
dispatch({ type: "INIT" }); // 初始化
if (typeof enhancer === "function") {
dispatchWithMiddleware = enhancer({
dispatch: action => dispatchWithMiddleware(action),
getState
})(dispatch);
}
return {
dispatch: dispatchWithMiddleware,
subscribe,
getState
};
}
写法上有区别,但核心内容上没有区别。
6. 注意
可能有些人已经注意到了奇怪的地方。
//为什么不直接传dispatch
{
dispatch, getState;
}
//而是传递匿名函数
{
dispatch: (action) => dispatchWithMiddleware(action), getState;
}
原因在于有些中间件会使用绑定的dispatch
方法,比如redux-thunk
,所以要保证传入的dispatch
与外部dispatchWithMiddleware
保持相同。
dispatchWithMiddleware
为引用类型,在绑定中间件后又会重新赋值,所以这里最好的解法就是“闭包”,将变量存在内存里维持起来。
所以这里传入函数,是为了触发闭包维持变量。