手写一Redux,深入理解其原理

开发 前端
Redux可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个Redux,以便于深入理解他的原理。

手写一Redux,深入理解其原理

Redux可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个Redux,以便于深入理解他的原理。我们还是老套路,从基本的用法入手,然后自己实现一个Redux来替代源码的NPM包,但是功能保持不变。本文只会实现Redux的核心库,跟其他库的配合使用,比如React-Redux准备后面单独写一篇文章来讲。有时候我们过于关注使用,只记住了各种使用方式,反而忽略了他们的核心原理,但是如果我们想真正的提高技术,最好还是一个一个搞清楚,比如Redux和React-Redux看起来很像,但是他们的核心理念和关注点是不同的,Redux其实只是一个单纯状态管理库,没有任何界面相关的东西,React-Redux关注的是怎么将Redux跟React结合起来,用到了一些React的API。

本文全部代码已经上传到GitHub,大家可以拿下来玩下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux

基本概念

Redux的概念有很多文章都讲过,想必大家都看过很多了,我这里不再展开,只是简单提一下。Redux基本概念主要有以下几个:

Store

人如其名,Store就是一个仓库,它存储了所有的状态(State),还提供了一些操作他的API,我们后续的操作其实都是在操作这个仓库。假如我们的仓库是用来放牛奶的,初始情况下,我们的仓库里面一箱牛奶都没有,那Store的状态(State)就是: 

  1.  
  2.     milk: 0  

Actions

一个Action就是一个动作,这个动作的目的是更改Store中的某个状态,Store还是上面的那个仓库,现在我想往仓库放一箱牛奶,那"我想往仓库放一箱牛奶"就是一个Action,代码就是这样: 

  1.  
  2.   type: "PUT_MILK",  
  3.   count: 1  

Reducers

前面"我想往仓库放一箱牛奶"只是想了,还没操作,具体操作要靠Reducer,Reducer就是根据接收的Action来改变Store中的状态,比如我接收了一个PUT_MILK,同时数量count是1,那放进去的结果就是milk增加了1,从0变成了1,代码就是这样: 

  1. const initState = {  
  2.   milk: 0  
  3.  
  4. function reducer(state = initState, action) {  
  5.   switch (action.type) {  
  6.     case 'PUT_MILK':  
  7.       return {...state, milk: state.milk + action.count}  
  8.     default:  
  9.       return state  
  10.   }  

可以看到Redux本身就是一个单纯的状态机,Store存放了所有的状态,Action是一个改变状态的通知,Reducer接收到通知就更改Store中对应的状态。

简单例子

下面我们来看一个简单的例子,包含了前面提到的Store,Action和Reducer这几个概念: 

  1. import { createStore } from 'redux';  
  2. const initState = {  
  3.   milk: 0  
  4. };  
  5. function reducer(state = initState, action) {  
  6.   switch (action.type) {  
  7.     case 'PUT_MILK': 
  8.        return {...state, milk: state.milk + action.count};  
  9.     case 'TAKE_MILK':  
  10.       return {...state, milk: state.milk - action.count};  
  11.     default:  
  12.       return state;  
  13.   }  
  14.  
  15. let store = createStore(reducer);  
  16. // subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用  
  17. // 如果是结合页面更新,更新的操作就是在这里执行  
  18. store.subscribe(() => console.log(store.getState()));  
  19. // 将action发出去要用dispatch  
  20. store.dispatch({ type: 'PUT_MILK' });    // milk: 1  
  21. store.dispatch({ type: 'PUT_MILK' });    // milk: 2  
  22. store.dispatch({ type: 'TAKE_MILK' });   // milk: 1 

自己实现

前面我们那个例子虽然短小,但是已经包含了Redux的核心功能了,所以我们手写的第一个目标就是替换这个例子中的Redux。要替换这个Redux,我们得先知道他里面都有什么东西,仔细一看,我们好像只用到了他的一个API:

createStore:这个API接受reducer方法作为参数,返回一个store,主要功能都在这个store上。

看看store上我们都用到了啥:

store.subscribe: 订阅state的变化,当state变化的时候执行回调,可以有多个subscribe,里面的回调会依次执行。

store.dispatch: 发出action的方法,每次dispatch action都会执行reducer生成新的state,然后执行subscribe注册的回调。

store.getState:一个简单的方法,返回当前的state。

看到subscribe注册回调,dispatch触发回调,想到了什么,这不就是发布订阅模式吗?我之前有一篇文章详细讲过发布订阅模式了,这里直接仿写一个。 

  1. function createStore() {  
  2.   let state;              // state记录所有状态  
  3.   let listeners = [];     // 保存所有注册的回调  
  4.   function subscribe(callback) {  
  5.     listeners.push(callback);       // subscribe就是将回调保存下来  
  6.   }  
  7.   // dispatch就是将所有的回调拿出来依次执行就行  
  8.   function dispatch() {  
  9.     for (let i = 0; i < listeners.length; i++) {  
  10.       const listener = listeners[i];  
  11.       listener();  
  12.     }  
  13.   }  
  14.   // getState直接返回state  
  15.   function getState() {  
  16.     return state;  
  17.   }  
  18.   // store包装一下前面的方法直接返回  
  19.   const store = {  
  20.     subscribe,  
  21.     dispatch,  
  22.     getState  
  23.   }  
  24.   return store;  

上述代码是不是很简单嘛,Redux核心也是一个发布订阅模式,就是这么简单!等等,好像漏了啥,reducer呢?reducer的作用是在发布事件的时候改变state,所以我们的dispatch在执行回调前应该先执行reducer,用reducer的返回值重新给state赋值,dispatch改写如下: 

  1. function dispatch(action) {  
  2.   state = reducer(state, action);  
  3.   for (let i = 0; i < listeners.length; i++) {  
  4.     const listener = listeners[i];  
  5.     listener();  
  6.   }  

到这里,前面例子用到的所有API我们都自己实现了,我们用自己的Redux来替换下官方的Redux试试: 

  1. // import { createStore } from 'redux';  
  2. import { createStore } from './myRedux'; 

可以看到输出结果是一样的,说明我们自己写的Redux没有问题:

了解了Redux的核心原理,我们再去看他的源码应该就没有问题了,createStore的源码传送门

最后我们再来梳理下Redux的核心流程,注意单纯的Redux只是个状态机,是没有View层的哦。

除了这个核心逻辑外,Redux里面还有些API也很有意思,我们也来手写下。

手写combineReducers

combineReducers也是使用非常广泛的API,当我们应用越来越复杂,如果将所有逻辑都写在一个reducer里面,最终这个文件可能会有成千上万行,所以Redux提供了combineReducers,可以让我们为不同的模块写自己的reducer,最终将他们组合起来。比如我们最开始那个牛奶仓库,由于我们的业务发展很好,我们又增加了一个放大米的仓库,我们可以为这两个仓库创建自己的reducer,然后将他们组合起来,使用方法如下: 

  1. import { createStore, combineReducers } from 'redux';  
  2. const initMilkState = {  
  3.   milk: 0  
  4. };  
  5. function milkReducer(state = initMilkState, action) {  
  6.   switch (action.type) {  
  7.     case 'PUT_MILK':  
  8.       return {...state, milk: state.milk + action.count};  
  9.     case 'TAKE_MILK':  
  10.       return {...state, milk: state.milk - action.count};  
  11.     default:  
  12.       return state;  
  13.   }  
  14.  
  15. const initRiceState = {  
  16.   rice: 0  
  17. };  
  18. function riceReducer(state = initRiceState, action) {  
  19.   switch (action.type) {  
  20.     case 'PUT_RICE':  
  21.       return {...state, rice: state.rice + action.count};  
  22.     case 'TAKE_RICE':  
  23.       return {...state, rice: state.rice - action.count};  
  24.     default:  
  25.       return state;  
  26.   }  
  27.  
  28. // 使用combineReducers组合两个reducer  
  29. const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer});  
  30. let store = createStore(reducer);  
  31. store.subscribe(() => console.log(store.getState()));  
  32. // 操作🥛的action  
  33. store.dispatch({ type: 'PUT_MILK', count: 1 });    // milk: 1  
  34. store.dispatch({ type: 'PUT_MILK', count: 1 });    // milk: 2  
  35. store.dispatch({ type: 'TAKE_MILK', count: 1 });   // milk: 1  
  36. // 操作大米的action  
  37. store.dispatch({ type: 'PUT_RICE', count: 1 });    // rice: 1  
  38. store.dispatch({ type: 'PUT_RICE', count: 1 });    // rice: 2  
  39. store.dispatch({ type: 'TAKE_RICE', count: 1 });   // rice: 1 

上面代码我们将大的state分成了两个小的milkState和riceState,最终运行结果如下:

知道了用法,我们尝试自己来写下呢!要手写combineReducers,我们先来分析下他干了啥,首先它的返回值是一个reducer,这个reducer同样会作为createStore的参数传进去,说明这个返回值是一个跟我们之前普通reducer结构一样的函数。这个函数同样接收state和action然后返回新的state,只是这个新的state要符合combineReducers参数的数据结构。我们尝试来写下: 

  1. function combineReducers(reducerMap) {  
  2.   const reducerKeys = Object.keys(reducerMap);    // 先把参数里面所有的键值拿出来  
  3.   // 返回值是一个普通结构的reducer函数  
  4.   const reducer = (state = {}, action) => {  
  5.     const newState = {};    
  6.     for(let i = 0; i < reducerKeys.length; i++) {  
  7.       // reducerMap里面每个键的值都是一个reducer,我们把它拿出来运行下就可以得到对应键新的state值  
  8.       // 然后将所有reducer返回的state按照参数里面的key组装好  
  9.       // 最后再返回组装好的newState就行  
  10.       const key = reducerKeys[i];  
  11.       const currentReducer = reducerMap[key];  
  12.       const prevState = state[key]; 
  13.        newState[key] = currentReducer(prevState, action);  
  14.     }  
  15.      return newState;  
  16.   };   
  17.   return reducer;  

官方源码的实现原理跟我们的一样,只是他有更多的错误处理,大家可以对照着看下。

手写applyMiddleware

middleware是Redux里面很重要的一个概念,Redux的生态主要靠这个API接入,比如我们想写一个logger的中间件可以这样写(这个中间件来自于官方文档): 

  1. // logger是一个中间件,注意返回值嵌了好几层函数  
  2. // 我们后面来看看为什么这么设计  
  3. function logger(store) {  
  4.   return function(next) {  
  5.     return function(action) {  
  6.       console.group(action.type);  
  7.       console.info('dispatching', action);  
  8.       let result = next(action);  
  9.       console.log('next state', store.getState());  
  10.       console.groupEnd();  
  11.       return result  
  12.     }  
  13.   }  
  14. // 在createStore的时候将applyMiddleware作为第二个参数传进去  
  15. const store = createStore 
  16.   reducer,  
  17.   applyMiddleware(logger)  

可以看到上述代码为了支持中间件,createStore支持了第二个参数,这个参数官方称为enhancer,顾名思义他是一个增强器,用来增强store的能力的。官方对于enhancer的定义如下: 

  1. type StoreEnhancer = (next: StoreCreator) => StoreCreator 

上面的结构的意思是说enhancer作为一个函数,他接收StoreCreator函数作为参数,同时返回的也必须是一个StoreCreator函数。注意他的返回值也是一个StoreCreator函数,也就是我们把他的返回值拿出来继续执行应该得到跟之前的createStore一样的返回结构,也就是说我们之前的createStore返回啥结构,他也必须返回结构,也就是这个store: 

  1.  
  2.   subscribe,  
  3.   dispatch,  
  4.   getState  

createStore支持enhancer

根据他关于enhancer的定义,我们来改写下自己的createStore,让他支持enhancer: 

  1. function createStore(reducer, enhancer) {   // 接收第二个参数enhancer  
  2.   // 先处理enhancer  
  3.   // 如果enhancer存在并且是函数  
  4.   // 我们将createStore作为参数传给他  
  5.   // 他应该返回一个新的createStore给我  
  6.   // 我再拿这个新的createStore执行,应该得到一个store  
  7.   // 直接返回这个store就行  
  8.   if(enhancer && typeof enhancer === 'function'){  
  9.     const newCreateStore = enhancer(createStore);  
  10.     const newStore = newCreateStore(reducer);  
  11.     return newStore;  
  12.   }  
  13.   // 如果没有enhancer或者enhancer不是函数,直接执行之前的逻辑  
  14.   // 下面这些代码都是之前那版  
  15.   // 省略n行代码  
  16.     // .......  
  17.   const store = {  
  18.     subscribe,  
  19.     dispatch,  
  20.     getState  
  21.   }  
  22.   return store;  

这部分对应的源码看这里。

applyMiddleware返回值是一个enhancer

前面我们已经有了enhancer的基本结构,applyMiddleware是作为第二个参数传给createStore的,也就是说他是一个enhancer,准确的说是applyMiddleware的返回值是一个enhancer,因为我们传给createStore的是他的执行结果applyMiddleware(): 

  1. function applyMiddleware(middleware) {  
  2.   // applyMiddleware的返回值应该是一个enhancer  
  3.   // 按照我们前面说的enhancer的参数是createStore  
  4.   function enhancer(createStore) { 
  5.      // enhancer应该返回一个新的createStore  
  6.     function newCreateStore(reducer) {  
  7.       // 我们先写个空的newCreateStore,直接返回createStore的结果  
  8.       const store = createStore(reducer);  
  9.       return store  
  10.     }    
  11.     return newCreateStore;  
  12.   } 
  13.    return enhancer;  

实现applyMiddleware

上面我们已经有了applyMiddleware的基本结构了,但是功能还没实现,要实现他的功能,我们必须先搞清楚一个中间件到底有什么功能,还是以前面的logger中间件为例: 

  1. function logger(store) {  
  2.   return function(next) {  
  3.     return function(action) {  
  4.       console.group(action.type);  
  5.       console.info('dispatching', action);  
  6.       let result = next(action);  
  7.       console.log('next state', store.getState());  
  8.       console.groupEnd();  
  9.       return result  
  10.     }  
  11.   }  

这个中间件运行效果如下:

可以看到我们let result = next(action);这行执行之后state改变了,前面我们说了要改变state只能dispatch(action),所以这里的next(action)就是dispatch(action),只是换了一个名字而已。而且注意最后一层返回值return function(action)的结构,他的参数是action,是不是很像dispatch(action),其实他就是一个新的dispatch(action),这个新的dispatch(action)会调用原始的dispatch,并且在调用的前后加上自己的逻辑。所以到这里一个中间件的结构也清楚了:

  1.   一个中间件接收store作为参数,会返回一个函数
  2.   返回的这个函数接收老的dispatch函数作为参数,会返回一个新的函数
  3.   返回的新函数就是新的dispatch函数,这个函数里面可以拿到外面两层传进来的store和老dispatch函数

所以说白了,中间件就是加强dispatch的功能,用新的dispatch替换老的dispatch,这不就是个装饰者模式吗?其实前面enhancer也是一个装饰者模式,传入一个createStore,在createStore执行前后加上些代码,最后又返回一个增强版的createStore。可见设计模式在这些优秀的框架中还真是广泛存在,如果你对装饰者模式还不太熟悉,可以看我之前这篇文章。

遵循这个思路,我们的applyMiddleware就可以写出来了: 

  1. // 直接把前面的结构拿过来  
  2. function applyMiddleware(middleware) {  
  3.   function enhancer(createStore) {  
  4.     function newCreateStore(reducer) {  
  5.       const store = createStore(reducer);   
  6.       // 将middleware拿过来执行下,传入store  
  7.       // 得到第一层函数 
  8.        const func = middleware(store);     
  9.        // 解构出原始的dispatch  
  10.       const { dispatch } = store;      
  11.        // 将原始的dispatch函数传给func执行  
  12.       // 得到增强版的dispatch  
  13.       const newDispatch = func(dispatch);      
  14.        // 返回的时候用增强版的newDispatch替换原始的dispatch  
  15.       return {...store, dispatch: newDispatch}  
  16.     }    
  17.      return newCreateStore;  
  18.   } 
  19.    return enhancer;  

照例用我们自己的applyMiddleware替换老的,跑起来是一样的效果,说明我们写的没问题,哈哈~

支持多个middleware

我们的applyMiddleware还差一个功能,就是支持多个middleware,比如像这样: 

  1. applyMiddleware(  
  2.   rafScheduler,  
  3.   timeoutScheduler,  
  4.   thunk,  
  5.   vanillaPromise,  
  6.   readyStatePromise,  
  7.   logger,  
  8.   crashReporter  

其实要支持这个也简单,我们返回的newDispatch里面依次的将传入的middleware拿出来执行就行,多个函数的串行执行可以使用辅助函数compose,这个函数定义如下。只是需要注意的是我们这里的compose不能把方法拿来执行就完了,应该返回一个包裹了所有方法的方法。 

  1. function compose(...func){  
  2.   return funcs.reduce((a, b) => (...args) => a(b(...args)));  

这个compose可能比较让人困惑,我这里还是讲解下,比如我们有三个函数,这三个函数都是我们前面接收dispatch返回新dispatch的方法: 

  1. const fun1 = dispatch => newDispatch1;  
  2. const fun2 = dispatch => newDispatch2;  
  3. const fun3 = dispatch => newDispatch3; 

当我们使用了compose(fun1, fun2, fun3)后执行顺序是什么样的呢? 

  1. // 第一次其实执行的是  
  2. (func1, func2) => (...args) => func1(fun2(...args))  
  3. // 这次执行完的返回值是下面这个,用个变量存起来吧  
  4. const temp = (...args) => func1(fun2(...args))  
  5. // 我们下次再循环的时候其实执行的是  
  6. (temp, func3) => (...args) => temp(func3(...args));  
  7. // 这个返回值是下面这个,也就是最终的返回值,其实就是从func3开始从右往左执行完了所有函数  
  8. // 前面的返回值会作为后面参数 
  9.  (...args) => temp(func3(...args));  
  10. // 再看看上面这个方法,如果把dispatch作为参数传进去会是什么效果  
  11. (dispatch) => temp(func3(dispatch)); 
  12. // 然后func3(dispatch)返回的是newDispatch3,这个又传给了temp(newDispatch3),也就是下面这个会执行  
  13. (newDispatch3) => func1(fun2(newDispatch3))  
  14. // 上面这个里面用newDispatch3执行fun2(newDispatch3)会得到newDispatch2  
  15. // 然后func1(newDispatch2)会得到newDispatch1  
  16. // 注意这时候的newDispatch1其实已经包含了newDispatch3和newDispatch2的逻辑了,将它拿出来执行这三个方法就都执行了 

更多关于compose原理的细节可以看我之前这篇文章。

所以我们支持多个middleware的代码就是这样: 

  1. // 参数支持多个中间件  
  2. function applyMiddleware(...middlewares) {  
  3.   function enhancer(createStore) {  
  4.     function newCreateStore(reducer) {  
  5.       const store = createStore(reducer);      
  6.       // 多个middleware,先解构出dispatch => newDispatch的结构  
  7.       const chain = middlewares.map(middleware => middleware(store));
  8.        const { dispatch } = store;       
  9.        // 用compose得到一个组合了所有newDispatch的函数  
  10.       const newDispatchGen = compose(...chain);  
  11.       // 执行这个函数得到newDispatch  
  12.       const newDispatch = newDispatchGen(dispatch);  
  13.       return {...store, dispatch: newDispatch}  
  14.     }    
  15.      return newCreateStore;  
  16.   } 
  17.    return enhancer; 
  18.  

最后我们再加一个logger2中间件实现效果: 

  1. function logger2(store) {  
  2.   return function(next) {  
  3.     return function(action) {  
  4.       let result = next(action);  
  5.       console.log('logger2');  
  6.       return result  
  7.     }  
  8.   }  
  9. let store = createStore(reducer, applyMiddleware(logger, logger2)); 

可以看到logger2也已经打印出来了,大功告成。

现在我们也可以知道他的中间件为什么要包裹几层函数了:

第一层:目的是传入store参数

第二层:第二层的结构是dispatch => newDispatch,多个中间件的这层函数可以compose起来,形成一个大的dispatch => newDispatch

第三层:这层就是最终的返回值了,其实就是newDispatch,是增强过的dispatch,是中间件的真正逻辑所在。

到这里我们的applyMiddleware就写完了,对应的源码可以看这里,相信看了本文再去看源码就没啥问题了!

本文所有代码已经传到GitHub,大家可以去拿下来玩一下:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux

总结

  1.  单纯的Redux只是一个状态机,store里面存了所有的状态state,要改变里面的状态state,只能dispatch action。
  2.  对于发出来的action需要用reducer来处理,reducer会计算新的state来替代老的state。
  3.  subscribe方法可以注册回调方法,当dispatch action的时候会执行里面的回调。
  4.  Redux其实就是一个发布订阅模式!
  5.  Redux还支持enhancer,enhancer其实就是一个装饰者模式,传入当前的createStore,返回一个增强的createStore。
  6.  Redux使用applyMiddleware支持中间件,applyMiddleware的返回值其实就是一个enhancer。
  7.  Redux的中间件也是一个装饰者模式,传入当前的dispatch,返回一个增强了的dispatch。
  8.  单纯的Redux是没有View层的,所以他可以跟各种UI库结合使用,比如react-redux,计划下一篇文章就是手写react-redux。 

 

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2022-04-26 08:32:36

CSS前端

2021-12-01 19:32:14

原理Node代码

2021-07-16 07:57:34

ReduxDOM组件

2021-03-10 10:55:51

SpringJava代码

2022-11-04 09:43:05

Java线程

2024-03-12 00:00:00

Sora技术数据

2022-09-05 08:39:04

kubernetesk8s

2020-08-10 18:03:54

Cache存储器CPU

2023-09-19 22:47:39

Java内存

2020-03-26 16:40:07

MySQL索引数据库

2022-09-26 08:01:31

线程LIFO操作方式

2022-01-14 12:28:18

架构OpenFeign远程

2021-09-26 05:03:31

数据流Redux

2020-03-17 08:36:22

数据库存储Mysql

2022-09-05 22:22:00

Stream操作对象

2019-07-01 13:34:22

vue系统数据

2023-10-13 13:30:00

MySQL锁机制

2020-11-04 15:35:13

Golang内存程序员

2020-03-18 13:40:03

Spring事数据库代码

2015-03-17 09:44:08

点赞
收藏

51CTO技术栈公众号