本文主要是说一说怎么通过自己的理解来实现一个“简易”的redux,目的不是copy一个redux出来,而是动手实现redux的核心功能,从而帮助我们理解和使用redux。
事实上,redux的核心功能代码并不多,其他大量的代码都是为了应对实际使用中“不按套路出牌”的情况,所以为了便于理解,我们只实现核心功能不处理特殊情况和异常。
最后,我们也会参看redux的源码,来理解和学习redux是如何实现的。
理解redux
首先,我们按照自己的理解来梳理一下redux。
- redux本质上就是一个容器,我们可以将应用中所有需要使用的状数据都存放在容器里面。 在js里,我们直接用一个对象来表示容器就行了。
- 通过对容器添加订阅函数,当容器的数据变更时,我们将接收到响应从而进行相应的处理。
- 通过向容器发送一个action对象,来通知容器对数据进行变更。
- 容器通过调用我们编写的reducer函数,得到最新的状态数据,替换掉旧的状态数据。
实现createStore
第一版实现了redux最主要的api:createStore。这个函数返回我们通常所说的store对象,我们要实现的store对象上包含3个方法。分别是
- getState。返回store当前的状态state。
- subscribe。用于向store添加订阅函数。
- dispatch。用于向store发送变更的指令action对象。
下面是代码
{
const createStore = (reducer, prelodedState) => {
let state = prelodedState;
// 存放所有订阅函数
const listeners = [];
// 获取当前的state
const getState = () => state;
const dispatch = action => {
// 将当前的state和action传入reducer,计算出变更后的state
state = reducer(state, action);
// state变更后,遍历执行所有订阅函数
listeners.forEach(listener => listener());
}
const subscribe = listener => {
listeners.push(listener);
// 返回函数,用于移除订阅
return () => {
const i = listeners.indexOf(listener);
listeners.splice(i, 1);
}
}
// 创建store之后,初始化state
dispatch({});
return {
getState,
dispatch,
subscribe,
}
}
window.Redux = {
createStore,
}
}
下面是使用上述redux实现的计数器Counter例子
Redux_Counter
加入combineReducers
在计数器中,state只是一个单一的number数据类型,reducer也很简单,但是在实际应用中,state往往是一个复杂的对象,同时需要多个reducer来分别计算state下面对应的部分。
以todo为例:
它的state和reducer可能长这样
const state = {
todo: [
{
text: '吃饭',
completed: true,
},
{
text: '睡觉',
completed: false,
}
],
filter: 'FILTER_ALL', // 显示所有,不管是否完成
}
const reducer = (state = { todo: [], filter: 'FILTER_ALL' }, action) => {
switch (action.type) {
case 'TODO_ADD':
return {
...state,
todo: state.todo.concat({text: action.text, completed: false}),
}
// TODO_REMOVE ...
// TODO_TOGGLE ...
case 'FILTER_SET':
return {
todo: state.todo.slice(),
filter: action.filter,
}
default:
return state;
}
}
我们可以看到state主要分为todo列表和过滤器两部分,在reducer中,两个部分的处理逻辑混合在了一起,处理TODO_ADD的逻辑还要通过解构state将filter一同返回,处理FILTER_SET的逻辑还要负责拷贝一个新的todo一同返回。
这样会导致处理不同state的代码混合在一起,增加了复杂性和代码冗余,所以有必要将reducer拆分为独立的函数,各自处理state中对应的数据。
首先试一下手动合并多个reducer
// reducer:处理state下的数组todo
const todo = (state = [], action) => {
switch (action.type) {
case 'TODO_ADD':
return [...state, { text: action.text, completed: false }];
// TODO_REMOVE TODO_TOGGLE
default:
return state;
}
}
// reducer:处理state下的过滤器filter
const filter = (state = 'FILTER_ALL', action) => {
switch (action.type) {
case 'FILTER_SET':
return action.filter;
default:
return state;
}
}
// 手动合并reducer,将state下的数据拆开分别调用对应的处理函数,
// 最终组合成一个新的state返回
const reducer = (state = {}, action) => {
return {
todo: todo(state.todo, action),
filter: filter(state.filter, action),
}
}
下面我们自己实现一个combineReducers函数,用于合并多个reducer
const combineReducers = (reducers) => {
return (state = {}, action) => {
// 获取reducers的所有健值
const keys = Object.keys(reducers);
// 传入{}作为初始值(新的state)
return keys.reduce((prevState, key) => {
// 将key对应的旧的状态state[key]和action传入reducers中key对应的value处理函数
// 计算出新的state
prevState[key] = reducers[key](state[key], action);
return prevState;
}, {});
}
}
下面是加入combineReducers函数后,实现的todo例子
Redux_Todo
加入扩展机制
如果仅仅只有上面所说的功能,肯定是满足不了实际的需求的。比如需要统一规范地处理异步任务或者需要对某个api进行扩展或定制。
redux提供了两种扩展机制:中间件、增强器。
中间件是对dispatch方法的扩展,目前为止,如果调用dispatch传入一个action对象,这个action会直接抵达store对象,进而执行reducer并调用订阅函数。中间件就是在dispatch之后 action到达store之前对action进行解析处理的机制,经过一个个中间件函数处理之后,再将action传给store。
增强器可以对整个store进行扩展,而不仅仅是dispatch方法。
所以中间件就是一种增强器,中间件是通过增强器实现的,因为对dispatch方法的扩展比较常见和实用,所以将插入中间件的机制单独实现为applyMiddleware方法。
所以我们先看增强器怎么实现,然后再看怎么实现中间件。
加入增强器
超市总喜欢把散装的产用塑料盘子和保鲜膜包装一下再出售,包装过后的产品 颜值、便携和身价都得到了增强。
redux里的增强器也类似于这种包装机制,如果想对store的getState方法进行增强,就将它包装成一个新的函数,只要保证最终还是会调用store本来的getState方法就行了。
下面是对getState的增强,getState每次被调用的时候都会打印一句话
// 增强getState的增强器
const getStateEnhancer = (store) => {
const originalGetState = store.getState;
store.getState = () => {
console.log('----- getState is invoked -----');
return originalGetState();
}
}
// 创建store
const store = Redux.createStore(reducer, initialState);
// 增强store
getStateEnhancer(store);
这样虽然可以实现,但是太那啥了,后面我们会看看redux的源码是怎么实现的。
加入中间件
中间件是对dispatch方法的增强,也就是对dispatch方法进行包装,生成一个新的dispatch方法。
在新dispatch里面依次插入中间件函数,每个中间件都可以访问到getState、dispatch和action以及下一个中间件函数。
所以,在一个中间件内部通过解析action,中间件可以选择调用下一个中间件将action继续传递下去,也可以选择再次调用dispatch,让action重新在中间件中流转一遍。
最后一个中间件调用的下一个中间件函数指向包装之前的dispatch,这样action在经过中间件的处理之后,最终抵达store。
redux规定,中间件必须遵循如下所示的规范。
const middleware = ({dispatch, getState}) => next => action => {
// do something
next(action);
}
首先,中间件middleware必须是一个函数,这个函数会被注入一个对象作为参数,返回一个新的函数。新的函数的参数next代表下一个中间件,新函数再次返回一个函数,最后这个返回的函数才是中间件执行逻辑的地方,执行完以后调用next,把action传给下一个中间件,如果没有下一个中间件了,这个next就指向store原本的dispatch方法。
下面根据中间件的接口规范模拟实现的添加中间件的applyMiddleware方法
/**
* 组合函数,将多个函数组合为一个函数
* 比如a、b、c三个函数
* 执行 compose(a, b, c) 返回的函数近似于 (...args) => a(b(c(...args)))
* */
const compose = (...funcs) => {
if (funcs.length === 0) return f => f;
if (funcs.length === 1) return funcs[0];
return funcs.reduce((prevFunc, curFunc) => (...args) => prevFunc(curFunc(...args)));
}
/**
* 注入中间件
* @param {store} 需要注入中间件的store对象
* @param {...middlewares} 按顺序传入的中间件
*/
const applyMiddleware = (store, ...middlewares) => {
let dispatch;
// 注入中间件的参数对象,里面的dispatch指向新的dispatch函数
const injectApi = {
dispatch: (...args) => dispatch(...args),
getState: store.getState,
}
// 执行map之前,每个middleware大约长这样:({dispatch, getState}) => next => action => {};
// 对每个中间件注入参数调用以后,大约长这样:next => action => {};
const chain = middlewares.map(middleware => middleware(injectApi));
// 得到新的dispatch方法
store.dispatch = compose(...chain)(store.dispatch);
dispatch = store.dispatch;
}
// 测试中间件,只打印一句话
const middleware_1 = ({ dispatch, getState }) => next => action => {
console.log('middleware_1');
next(action);
}
const middleware_2 = ({ dispatch, getState }) => next => action => {
console.log('middleware_2');
next(action);
}
const middleware_3 = ({ dispatch, getState }) => next => action => {
console.log('middleware_3');
next(action);
}
// 使用式例
// 创建store
const store = Redux.createStore(reducer, initialState);
// 注入中间件
applyMiddleware(store, middleware_1, middleware_2, middleware_3);
对于applyMiddleware,我们重点看一下下面这句代码
store.dispatch = compose(...chain)(store.dispatch);
也就是多个中间件函数是怎么组合成一个函数,并且怎么和dispatch联系在一起的。
// 假如现在有上面所说的三个中间件 middleware_1、middleware_2 和 middleware_3
// 执行下面这句代码以后
// const chain = middlewares.map(middleware => middleware(injectApi));
// chain 大概长下面这样
chain = [
next => action => { console.log('middleware_1'); next(action); }, // middleware_1
next => action => { console.log('middleware_2'); next(action); }, // middleware_2
next => action => { console.log('middleware_3'); next(action); }, // middleware_3
]
// 执行了 compose(...chain) 以后
// compose(...chain)返回的函数大概长下面这样
(...args) => {
return ((...args) => {
return middleware_1(middleware_2(...args))
})(middleware_3(...args))
}
// 紧接着 (store.dispatch) 调用该返回函数的时候
// store.dispatch 传入 middleware_3,middleware_3变成下面这样
action => { console.log('middleware_3'); store.dispatch(action); }
// 变换后的middleware_3作为参数传入middleware_2,middleware_2变成下面这样
action => { console.log('middleware_2'); middleware_3(action); }
// 变换后的middleware_2作为参数传入middleware_1,middleware_1变成下面这样
action => { console.log('middleware_1'); middleware_2(action); }
// 所以 compose(...chain)(store.dispatch); 最终返回的函数是如下所示的middleware_1
action => { console.log('middleware_1'); middleware_2(action); }
// 然后在函数内部再调用middleware_2,middleware_2在内部再去调用middleware_3
// 这样就实现了中间件的顺序执行,并且最后一个中间件将调用 旧的dispatch函数
下面,我们在计数器Counter中添加 日志中间件 和 thunk中间件,看一下最终的效果
Redux_middleware_Counter
参看redux源码
combineReducers
这个函数里面需要关注的一点就是对于state是否变化的处理。
// 只保留了该方法的核心代码
function combineReducers(reducers) {
...
return function combination(state = {}, action) {
// 标志state是否改变
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)
nextState[key] = nextStateForKey
// 对于前后两次同一个key对应的state值,采用浅比较的方式
// 如果是同一个引用,就认为没有改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
// 有一个key对应的value改变了就返回新的state
// 所有key对应的value都没改变才使用旧的state
return hasChanged ? nextState : state
}
}
combineReducers采用浅比较的方式判断是返回新的state还是旧的state,如果根state下的每个属性值前后两次都是同一个引用的话,就将返回旧的state。
这也是redux所说的不直接修改state的原因,因为像react-redux这样的绑定库也采用浅比较的方式来判断state是否变化,如果直接修改state会导致react-redux认为state没有变化,从而不会触发渲染。
增强器
先看看一个什么都不做的增强器的格式
const enhancer = createStore => (reducer, prelodedState, enhancer) => {
const store = createStore(reducer, prelodedState, enhancer);
// 增强store的代码
return store;
}
redux的增强器
// 只保留了增强器相关的代码
// createStore可以接收3个参数
// 第一个参数永远是reducer,第二个和第三个是可选参数
// 第二个参数如果是函数就当作enhancer,否则作为state的初始状态
// 第三个参数如果有的话,必须是enhancer函数类型
function createStore(reducer, preloadedState, enhancer) {
...
// 如果有增强器
if (typeof enhancer !== 'undefined') {
// 将自己传给enhancer,得到一个新的createStore函数
// 然后再把剩余的两个参数传给新的createStore
return enhancer(createStore)(reducer, preloadedState)
}
...
return {
dispatch,
getState,
...
}
}
最后
redux还有很多方法实现这里并没有一一列举出来,如果有兴趣可以继续深入。
在看源码的时候,可以先把与核心功能无关的代码注释掉,这样看起来会轻松一些。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。