跳到主要内容

如何实现 Redux

· 阅读需 11 分钟
youniaogu

如何实现 Redux

示例 demo:github

注意:为了简单明了的展示主要流程,下面示例中均会省略异常情况的处理。

redux里有四个核心的 api,下面会按顺序介绍并实现它们

  1. combineReducer
  2. compose
  3. createStore
  4. 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为必传项,preloadedStateenhancer为非必传,当只传递两个参数时,createStore会检查第二个参数的类型,如何是 function 类型的话按enhancer处理,其他情况会当作preloadedState

  • reducer:响应 action,更新 state
  • preloadState:初始化 state
  • enhancerapplyMiddlewares返回的函数,用于增强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,
};
}

确认dispatchgetStatesubscribe的作用

  • dispatch:触发一个动作,修改 state,并触发所有的监听函数
  • getState:返回当前的 state
  • subscribe:订阅监听函数,返回取消订阅方法
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为引用类型,在绑定中间件后又会重新赋值,所以这里最好的解法就是“闭包”,将变量存在内存里维持起来。

所以这里传入函数,是为了触发闭包维持变量。

参考资料

Redux 从设计到源码

图解 Redux 中 middleware 的洋葱模型