【THE LAST TIME】从 Redux 源码中学习它的范式

THE LAST TIME The last time, I have learned 【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。 也是给自己的查缺补漏和技术分享。 笔者文章集合详见: GitHub 地址:Nealyang/personalBlog 公众号:全栈前端精选 TLT往期 彻底吃透 JavaScript 执行机制 this:call、apply、bind 一文吃透所有JS原型相关知识点 深入浅出 JavaScript 模块化 TypeScript进阶 之 重难点梳理 前言 范式概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:按既定的用法,范式就是一种公认的模型或模式。 而学习 Redux,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。 讲真,标题真的是不好取,因为本文是我写的 redux 的下一篇。两篇凑到一起,才是完整的 Redux。 上篇:从 Redux 设计理念到源码分析 本文续上篇,接着看 combineReducers、applyMiddleware和 compose 的设计与源码实现 至于手写,其实也是非常简单,说白了,去掉源码中严谨的校验,就是市面上手写了。当然,本文,我也尽量以手写演进的形式,去展开剩下几个 api 的写法介绍。 combineReducers 从上一篇中我们知道,newState 是在 dispatch 的函数中,通过 currentReducer(currentState,action)拿到的。所以 state 的最终组织的样子,完全的依赖于我们传入的 reducer。而随着应用的不断扩大,state 愈发复杂,redux 就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 or func 中处理,然后再来组织合并。(模块化有么有) combineReducers 并不是 redux 的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。 进化过程 比如我们现在需要管理这么一个"庞大"的 state: 庞大的 state let state={ name:'Nealyang', baseInfo:{ age:'25', gender:'man' }, other:{ github:'https://github.com/Nealyang', WeChatOfficialAccount:'全栈前端精选' } } 因为太庞大了,写到一个 reducer 里面去维护太难了。所以我拆分成三个 reducer。 function nameReducer(state, action) { switch (action.type) { case "UPDATE": return action.name; default: return state; } } function baseInfoReducer(state, action) { switch (action.type) { case "UPDATE_AGE": return { ...state, age: action.age, }; case "UPDATE_GENDER": return { ...state, age: action.gender, }; default: return state; } } function otherReducer(state,action){...} 为了他这个组成一个我们上文看到的 reducer,我们需要搞个这个函数 const reducer = combineReducers({ name:nameReducer, baseInfo:baseInfoReducer, other:otherReducer }) 所以,我们现在自己写一个 combineReducers function combineReducers(reducers){ const reducerKeys = Object.keys(reducers); return function (state={},action){ const nextState = {}; for(let i = 0,keyLen = reducerKeys.length;i = ( state: S | undefined, action: A ) => S export type ReducersMapObject = { [K in keyof S]: Reducer } 定义了一个需要传递给 combineReducers 函数的参数类型。也就是我们上面的 { name:nameReducer, baseInfo:baseInfoReducer, other:otherReducer } 其实就是变了一个 state 的 key,然后 key 对应的值是这个 Reducer,这个 Reducer 的 state 是前面取出这个 key 的state 下的值。 export default function combineReducers(reducers: ReducersMapObject) { //获取所有的 key,也就是未来 state 的 key,同时也是此时 reducer 对应的 key const reducerKeys = Object.keys(reducers) // 过滤一遍 reducers 对应的 reducer 确保 kv 格式么有什么毛病 const finalReducers: ReducersMapObject = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (process.env.NODE_ENV !== 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } // 再次拿到确切的 keyArray const finalReducerKeys = Object.keys(finalReducers) // This is used to make sure we don't warn about the same // keys multiple times. let unexpectedKeyCache: { [key: string]: true } if (process.env.NODE_ENV !== 'production') { unexpectedKeyCache = {} } let shapeAssertionError: Error try { // 校验自定义的 reducer 一些基本的写法 assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } // 重点是这个函数 return function combination( state: StateFromReducersMapObject = {}, action: AnyAction ) { if (shapeAssertionError) { throw shapeAssertionError } if (process.env.NODE_ENV !== 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage( state, finalReducers, action, unexpectedKeyCache ) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState: StateFromReducersMapObject = {} 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) // 上面的部分都是我们之前手写内容,nextStateForKey 是返回的一个newState,判断不能为 undefined if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey // 判断是否改变,这里其实我还是很疑惑 // 理论上,reducer 后的 newState 无论怎么样,都不会等于 preState 的 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length return hasChanged ? nextState : state } } combineReducers 代码其实非常简单,核心代码也就是我们上面缩写的那样。但是我是真的喜欢这个功能。 image applyMiddleware 说 applyMiddleware 这个方法,其实不得不说,redux 中的 Middleware。中间件的概念不是 redux 独有的。Express、Koa等框架,也都有这个概念。只是为解决不同的问题而存在罢了。 Redux 的 Middleware 说白了就是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能! 一般我们常用的可以记录日志、错误采集、异步调用等。 其实关于Redux 的 Middleware, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍:Redux 中文文档 Middleware 演化过程 记录日志的功能增强 需求:在每次修改 state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state。 Action:每次修改都是 dispatch 发起的,所以这里我只要在 dispatch 加一层处理就一劳永逸了。 const store = createStore(reducer); const next = store.dispatch; /*重写了store.dispatch*/ store.dispatch = (action) => { console.log('this state', store.getState()); console.log('action', action); next(action); console.log('next state', store.getState()); } 如上,在我们每一次修改 dispatch 的时候都可以记录下来日志。因为我们是重写了 dispatch 不是。 增加个错误监控的增强 const store = createStore(reducer); const next = store.dispatch; store.dispatch = (action) => { try { next(action); } catch (err) { console.error('错误报告: ', err) } } 所以如上,我们也完成了这个需求。 但是,回头看看,这两个需求如何才能够同时实现,并且能够很好地解耦呢? 想一想,既然我们是增强 dispatch。那么是不是我们可以将 dispatch 作为形参传入到我们增强函数。 多文件增强 const exceptionMiddleware = (next) => (action) => { try { /*loggerMiddleware(action);*/ next(action); } catch (err) { console.error('错误报告: ', err) } } /*loggerMiddleware 变成参数传进去*/ store.dispatch = exceptionMiddleware(loggerMiddleware); // 这里额 next 就是最纯的 store.dispatch 了 const loggerMiddleware = (next) => (action) => { console.log('this state', store.getState()); console.log('action', action); next(action); console.log('next state', store.getState()); } 所以最终使用的时候就如下了 const store = createStore(reducer); const next = store.dispatch; const loggerMiddleware = (next) => (action) => { console.log('this state', store.getState()); console.log('action', action); next(action); console.log('next state', store.getState()); } const exceptionMiddleware = (next) => (action) => { try { next(action); } catch (err) { console.error('错误报告: ', err) } } store.dispatch = exceptionMiddleware(loggerMiddleware(next)); 但是如上的代码,我们又不能将 Middleware 独立到文件里面去,因为依赖外部的 store。所以我们再把 store 传入进去! const store = createStore(reducer); const next = store.dispatch; const loggerMiddleware = (store) => (next) => (action) => { console.log('this state', store.getState()); console.log('action', action); next(action); console.log('next state', store.getState()); } const exceptionMiddleware = (store) => (next) => (action) => { try { next(action); } catch (err) { console.error('错误报告: ', err) } } const logger = loggerMiddleware(store); const exception = exceptionMiddleware(store); store.dispatch = exception(logger(next)); 以上其实就是我们写的一个 Middleware,理论上,这么写已经可以满足了。但是!是不是有点不美观呢?且阅读起来非常的不直观呢? 如果我需要在增加个中间件,调用就成为了 store.dispatch = exception(time(logger(action(xxxMid(next))))) 这也就是 applyMiddleware 的作用所在了。 我们只需要知道有多少个中间件,然后在内部顺序调用就可以了不是 const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore); const store = newCreateStore(reducer) 手写 applyMiddleware const applyMiddleware = function (...middlewares) { // 重写createStore 方法,其实就是返回一个带有增强版(应用了 Middleware )的 dispatch 的 store return function rewriteCreateStoreFunc(oldCreateStore) { // 返回一个 createStore 供外部调用 return function newCreateStore(reducer, initState) { // 把原版的 store 先取出来 const store = oldCreateStore(reducer, initState); // const chain = [exception, time, logger] 注意这里已经传给 Middleware store 了,有了第一次调用 const chain = middlewares.map(middleware => middleware(store)); // 取出原先的 dispatch let dispatch = store.dispatch; // 中间件调用时←,但是数组是→。所以 reverse。然后在传入 dispatch 进行第二次调用。最后一个就是 dispatch func 了(回忆 Middleware 是不是三个括号~~~) chain.reverse().map(middleware => { dispatch = middleware(dispatch); }); store.dispatch = dispatch; return store; } } } 解释全在代码上了 其实源码里面也是这么个逻辑,但是源码实现更加的优雅。他利用了函数式编程的compose 方法。在看 applyMiddleware 的源码之前呢,先介绍下 compose 的方法吧。 compose 其实 compose 函数做的事就是把 var a = fn1(fn2(fn3(fn4(x)))) 这种嵌套的调用方式改成 var a = compose(fn1,fn2,fn3,fn4)(x) 的方式调用。 compose的运行结果是一个函数,调用这个函数所传递的参数将会作为compose最后一个参数的参数,从而像'洋葱圈'似的,由内向外,逐步调用。 export default function compose(...funcs: Function[]) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return (arg: T) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args: any) => a(b(...args))) } 哦豁!有点蒙有么有~ 函数式编程就是烧脑?且直接。所以爱的人非常爱。 compose是函数式编程中常用的一种组合函数的方式。 方法很简单,传入的形参是 func[],如果只有一个,那么直接返回调用结果。如果是多个,则funcs.reduce((a, b) => (...args: any) => a(b(...args))). 我们直接啃最后一行吧 import {componse} from 'redux' function add1(str) { return 1 + str; } function add2(str) { return 2 + str; } function add3(a, b) { return a + b; } let str = compose(add1,add2,add3)('x','y') console.log(str) //输出结果 '12xy' 输出 dispatch = compose(...chain)(store.dispatch) applyMiddleware 的源码最后一行是这个。其实即使我们上面手写的 reverse 部分。 reduce 是 es5 的数组方法了,对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。函数签名为:arr.reduce(callback[, initialValue]) 所以如若我们这么看: [func1,func2,func3].reduce(function(a,b){ return function(...args){ return a(b(...args)) } }) 所以其实就非常好理解了,每一次 reduce 的时候,callback 的a,就是一个a(b(...args))的 function,当然,第一次是 a 是 func1。后面就是无限的叠罗汉了。最终拿到的是一个 func1(func2(func3(...args)))的 function。 总结 所以回头看看,redux 其实就这么些东西,第一篇算是 redux 的核心,关于状态管理的思想和方式。第二篇可以理解为 redux 的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。 学习交流 关注公众号【全栈前端精选】,每日获取好文推荐 添加微信号:is_Nealyang(备注来源) ,入群交流 公众号【全栈前端精选】 个人微信【is_Nealyang】 image image

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):