山寨一个 redux
本文主要是说一说怎么通过自己的理解来实现一个“简易”的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还有很多方法实现这里并没有一一列举出来,如果有兴趣可以继续深入。
在看源码的时候,可以先把与核心功能无关的代码注释掉,这样看起来会轻松一些。