这是一篇浅入浅出的 Redux 实现过程的推演笔记!正常来说应该是要从源码下手开始解析,这里是逆向推演,假如有需求是要这么一个东西,那么该如何从零开始实现?
通过该笔记,更多的是希望自己能够多熟悉从无到有的开发思维,而非源码解析这种从有到有的轮子思维。
 
Rudex 介绍
首先确认目标,要写个什么样的东西。
官宣:A predictable state container for JavaScript apps. JavaScript 应用程序中可预测的状态容器。
通过这句话,挖掘一些关键点:
  - 1. 要有个状态:state
  - 2. 要有个(状态)容器:store
  - 3. 在状态改变的流程中,状态是可预测的,即:
    - 3.1 何时触发状态进行改变? dispatch,触发 state 的改变
    - 3.2 触发了什么改变? action,描述 state 该做什么修改
    - 3.3 状态做什么样改变? reducer,描述 state 的改变过程,传入改变前的 state,返回改变后的 state
reducer 是个纯函数很重要,能够消除其他影响让 state 的变化真正是可预测的。
有了这些关键点,接下来就是实现了
 
Redux 实现
被控对象(state)被包含在整个链路中,我们关心链路即可
 
既然是(状态)容器,那就得先有个容器,先实现个函数去创建容器,并且容器抛出方法以支持对容器内状态进行操作。
  1. /*
  2. * createStore 状态容器
  3. * @param reducers 容器总得需要知道是做什么样的改变
  4. * @param initialState 初始化(预置)的 state
  5. */
  6.  
  7. const createStore = (reducers, initialState) => {
  8. // 通过 reducer 取到改变后的值,重新存储
  9. let currentReducer = reducers;
  10.  
  11. // 存
  12. let currentState = initialState;
  13.  
  14. // 取
  15. const getState = () => {
  16. return currentState;
  17. };
  18.  
  19. // 改
  20. const dispatch = action => {
  21. currentState = currentReducer(currentState, action);
  22. return action;
  23. };
  24.  
  25. // 这里可能还需要可被观察的,留坑、不实现,有兴趣的看文章后的源码阅读链接
  26. return {
  27. getState,
  28. dispatch
  29. };
  30. };
至此,容器部分完成。
 
接下来看改变流程可预测的实现:
action 描述 state 该做什么修改,这仅仅是个对象而已,我们仅需要定义好格式,如下例子(比如数字的重置)
  1. /*
  2. * action
  3. * @property type 描述需要做什么操作
  4. * @property preload 预加载数据,包含 state 的新值
  5. */
  6.  
  7. const RESET = "RESET";
  8. const RESET_ACTION = {
  9. type: RESET,
  10. preload: {
  11. count: 0
  12. }
  13. };
reducer 描述状态做了什么改变,或者说是改变的流程。
  1. /*
  2. * reducer
  3. * @currentState 当前的 state,旧值
  4. * @action 该做什么修改的类型描述
  5. */
  6.  
  7. const reducer = (state = { count: 0 }, action) => {
  8. switch (action.type) {
  9. case RESET: {
  10. return {
  11. ...state,
  12. ...action.preload
  13. };
  14. }
  15. default: {
  16. return state;
  17. }
  18. }
  19. };
 
好了,先将上面三份代码合一起,试一试
  1. const store = createStore(reducer);
  2. store.dispatch({ type: RESET, preload: { count: 10 } });
  3. store.getState(); // output { count: 10}
  4. store.dispatch(RESET_ACTION);
  5. store.getState(); // output { count: 0}

流程是正常了,但情况稍微有点不对,如果在 dispatch 之前去 getState,那么 state 是 {},而不是给的初始值{count:0}。对于这点,只要在 createStore 时候的默认执行一次 dispatch,用以生成初始的 state tree。
 
在 createStore 的 return 之前加入 dispatch
  1. dispatch({ type: "@redux/INIT" });
 
ok,至此已经有个简版 redux 了(observable 和 subscribe 的话,加个 listeners,这里不多做介绍,有兴趣的点击文末链接),但是 redux 还有 middleware 的功能(而且这部分代码会比这里好玩一点),继续实现 middleware
 
Middleware 实现
首先,还是先来明确一下希望 middleware 能够帮助我们做什么?
官宣:
Middleware is the suggested way to extend Redux with custom functionality. Middleware lets you wrap the store's dispatch method for fun and profit. The key feature of middleware is that it is composable. Multiple middleware can be combined together, where each middleware requires no knowledge of what comes before or after it in the chain.
翻译一下:
Middleware 是通过自定义功能来扩展 redux 的推荐方法,它能够让你有效的包裹 store 的 dispatch 方法已达到所需的目的,其关键特征在于组合,多个 middleware 能够进行组合,每个 middleware 都是独立的,它们不需要知道在流程的之前或之后会发生什么。
从上面的描述中,得出结论:
  - 首先,middleware 要是个 function 且该函数对 dispatch 的执行做包裹;
  - 然后,每个 middleware 互不相干且可组合的;
  - 最后,值得注意的是 middleware 内部能够访问及操作 state,不然只能做些和 state 不相干的事情,那这个扩展的意义就不大了。
 
接下来是推演实现的过程:
 
首先,我们需要考虑的是怎么处理每个函数,让其既是独立的,又是可组合的,而且内部还得包裹 dispatch。看下面的思考过程:
 
比如我有个函数 a 和函数 dispatch 我们希望执行的过程是 dispatch 被包裹在 a 内部执行,首先想到的肯定是 callback 形式,没毛病,看代码
  1. var a = function(next) {
  2. console.log("a-before");
  3. next();
  4. console.log("a-after");
  5. };
  6. var dispatch = function(action) {
  7. console.log("do ", action);
  8. return action;
  9. };
  10.  
  11. a(dispatch);
  12. // output:
  13. // a-before
  14. // do undefined
  15. // a-after
但是没有能够把 dispatch 的参数传进去呀,于是对于内部函数的传参,不陌生的,我们又想到,外包再套一层 function,闭包存参(用 jqyery 的时候绑定事件没少这么干吧),看代码:
  1. var a = function(next) {
  2. return function(action) {
  3. console.log("a-before");
  4. next(action);
  5. console.log("a-after");
  6. };
  7. };
  8. var dispatch = function(action) {
  9. console.log("do ", action);
  10. return action;
  11. };
  12.  
  13. a(dispatch)("test action");
  14. // output:
  15. // a-before
  16. // do test action
  17. // a-after
但如果 a 的这种类型的包裹函数是多个的,试下加个函数 b,因为要嵌套的是函数,所以将 action 作为第二次执行的参数
  1. var a = function(next) {
  2. return function(action) {
  3. console.log("a-before");
  4. next(action);
  5. console.log("a-after");
  6. };
  7. };
  8. var b = function(next) {
  9. return function(action) {
  10. console.log("b-before");
  11. next(action);
  12. console.log("b-after");
  13. };
  14. };
  15. var dispatch = function(action) {
  16. console.log("do ", action);
  17. return action;
  18. };
  19.  
  20. a(b(dispatch))("test action");
  21. // output:
  22. // a-before
  23. // b-before
  24. // do test action
  25. // b-after
  26. // a-after
 
然后问题又来了 ,我们要再加个函数 c,难道让我写 a(b(c(dispacth)))(action)?
既然函数 a, b, c 都是一种类型的东西,可以格式化成数组,回想一下什么方法能够依次组合数组的每一项。没错,是 reduce,继续看代码:
  1. var a = function(next) {
  2. return function(action) {
  3. console.log("a-before");
  4. next(action);
  5. console.log("a-after");
  6. };
  7. };
  8. var b = function(next) {
  9. return function(action) {
  10. console.log("b-before");
  11. next(action);
  12. console.log("c-after");
  13. };
  14. };
  15. var c = function(next) {
  16. return function(action) {
  17. console.log("c-before");
  18. next(action);
  19. console.log("c-after");
  20. };
  21. };
  22. var dispatch = function(action) {
  23. console.log("do ", action);
  24. return action;
  25. };
  26.  
  27. var d = [a, b, c].reduce((pre, now) => (...args) => pre(now(...args)));
  28.  
  29. d(dispatch)("test action");
  30. // output:
  31. // a-before
  32. // b-before
  33. // c-before
  34. // do test action
  35. // c-after
  36. // b-after
  37. // a-after
 
好了,想到了如何将 middleware 串起来和如何将 dispatch 封装的方法后,集成到 redux 的代码里试试
单独抽一个 compose 函数用以处理一个或多个 middleware,代码如下:
  1. const compose = (...funcs) => {
  2. if (funcs.length === 0) {
  3. return arg => arg;
  4. }
  5.  
  6. if (funcs.length === 1) {
  7. return funcs[0];
  8. }
  9.  
  10. return funcs.reduce((a, b) => (...args) => a(b(...args)));
  11. };
 
修改 createStore 里的 dispatch 方法,让其支持当存在 middleware 的时候,dispatch 需要被重写。代码如下:
  1. /*
  2. * createStore 状态容器
  3. * @param reducers 容器总得需要知道是做什么样的改变
  4. * @param initialState 初始化(预置)的 state
  5. * @param enhancer 扩展的 middlewares
  6. */
  7.  
  8. const createStore = (reducers, initialState, enhancer) => {
  9. // 参数互换 如果 initialState 是个函数,enhancer = undefined 则 enhancer 和 initialState 互换
  10. if (typeof initialState === "function" && typeof enhancer === "undefined") {
  11. enhancer = initialState;
  12. initialState = undefined;
  13. }
  14.  
  15. // 如果有 middleware 的时候,则 createStore 稍后处理,处理详情参照 applyMiddleware 函数
  16. if (typeof enhancer !== "undefined" && typeof enhancer === "function") {
  17. // 为什么是这样写? 继续往下看
  18. return enhancer(createStore)(reducer, initialState);
  19. }
  20.  
  21. // ...
  22. // 之前的代码
  23. };
 
结合 createStore,注意到前文提出的,middleware 内部支持访问和操作 state,我们需要实现 createStore 里面的 enhancer 函数,就是函数 applyMiddleware,于是给出代码:
  1. /*
  2. * applyMiddleware 实现中间件的应用
  3. * @param ...middlewares 插入的 state 处理流程的中间件
  4. */
  5. const applyMiddleware = (...middlewares) => {
  6. // 传入 middlewares
  7. return createStore => (...args) => {
  8. const store = createStore(...args);
  9. // middleware 内部能做的 state 操作
  10. const middlewareAPI = {
  11. getState: store.getState,
  12. dispatch: (...args) => dispatch(...args)
  13. };
  14. // 将 middleware 处理,以 middlewareAPI 作为参数执行并且取到 middleware 的内部函数
  15. const chain = middlewares.map(middleware => middleware(middlewareAPI));
  16. // 进行 compose 组合
  17. // 如存在 3 个 middleware A(ABefore,AAfter) B(BBefore,BAfter) C(CBefore,CAfter)
  18. // 则执行顺序是 ABefore - BBefore - CBefore - (真实的操作) - CAfter - BAfter - AAfter
  19. dispatch = compose(...chain)(store.dispatch);
  20.  
  21. return {
  22. ...store,
  23. dispatch
  24. };
  25. };
  26. };
 
核心实现的代码写完了,然后测试一下?(注意:测试的时候需要在 createStore 的 dispatch 里加个 "do dispatch" 的 log,方便看执行流程。)
  1. const logger = ({ getState }) => {
  2. return next => action => {
  3. console.log("will dispatch", action);
  4.  
  5. const returnValue = next(action);
  6.  
  7. console.log("state after dispatch", getState());
  8.  
  9. return returnValue;
  10. };
  11. };
  12.  
  13. const store = createStore(reducer, applyMiddleware(logger));
  14.  
  15. store.dispatch(RESET_ACTION);
  16. // output
  17. // will dispatch {type: "RESET", preload: {…}}
  18. // do dispatch
  19. // state after dispatch {count: 0}
 
好了,来回顾一下实现过程,首先有个目标:做一个可预测的状态容器;然后分析目标,挖掘关键点,依次实现。实现的过程更多在于"语言运用的规范",技术知识点方面好像确实都是一些基础的运用哦。
最后,本文仅解析 redux 的实现思路,代码与源码并不完全相同。redux 不止这些代码(但其实也没多少其余代码),还有一些断言、错误提示、开发提示、bindActionCreators 啥的操作等等...
 
 

Redux 实现过程的推演的更多相关文章

  1. 从匿名方法到 Lambda 表达式的推演过程

    Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数. 通过使用 lambda 表达式,可以写入可作为参数传递或作为函数调用值返回的本地函数. 以上是msdn官网对Lambda 表达式 ...

  2. 从下往上看--新皮层资料的读后感 第三部分 70年前的逆向推演- 从NN到ANN

    第三部分 NN-ANN 70年前的逆向推演 从这部分开始,调整一下视角主要学习神经网络算法,将其与生物神经网络进行横向的比较,以窥探一二. 现在基于NN的AI应用几乎是满地都是,效果也不错,这种貌似神 ...

  3. Lambda 表达式推演全过程

    Java 的 Lambda 表达式推演过程: 第一步:正常的类实现(外部实现),new一个对象,然后重写方法实现 public class TestLambda3 { public static vo ...

  4. .NET 云原生架构师训练营(ASP .NET Core 整体概念推演)--学习笔记

    演化与完善整体概念 ASP .NET Core 整体概念推演 整体概念推演到具体的形式 ASP .NET Core 整体概念推演 ASP .NET Core 其实就是通过 web framework ...

  5. C语言 数组做函数参数退化为指针的技术推演

    //数组做函数参数退化为指针的技术推演 #include<stdio.h> #include<stdlib.h> #include<string.h> //一维数组 ...

  6. javascript基础修炼(4)——UMD规范的代码推演

    javascript基础修炼(4)--UMD规范的代码推演 1. UMD规范 地址:https://github.com/umdjs/umd UMD规范,就是所有规范里长得最丑的那个,没有之一!!!它 ...

  7. 【转】- 从FM推演各深度CTR预估模型(附代码)

    从FM推演各深度CTR预估模型(附代码) 2018年07月13日 15:04:34 阅读数:584 作者: 龙心尘 && 寒小阳 时间:2018年7月 出处: 龙心尘 寒小阳

  8. Android 进程常驻----native保活5.0以上方案推演过程以及代码

    正文: 上一篇我们通过父子进程间建立双管道,来监听进程死掉,经过测试,无耗电问题,无内存消耗问题,可以在设置中force close下成功拉起,也可以在获取到root权限的360/cleanmaste ...

  9. Android 进程常驻----native保活5.0以下方案推演过程以及代码

    正文: 今天继续昨天,一鼓作气,争取这个礼拜全部写完. 上一篇文章留了一个别人的github链接,他里面的native保活实现方案也是大多数公司采用的方案. 我们先来讲一下他的方案. 他是首先开启一个 ...

随机推荐

  1. CCS中cmd文件的编写

    http://blog.sina.com.cn/s/blog_abe5740601015b3q.html CMD的专业名称叫链接器配置文件,是存放链接器的配置信息的,我们简称为命令文件,其中比较关键的 ...

  2. AHOI——Day1个人感悟

    今天,是个bilibili的日子.(嗯?什么意思?) 洛谷已经尽力了: 于是我带着洛谷的祝福,来到了AHOI的考场--合肥一中. 其实我是考完才签到的,我一大早五点多就起来了,到考场后,在肯德基吃了早 ...

  3. 02.02.02 第2章 制作power bi图表(Power BI商业智能分析)

    ---恢复内容开始--- 02.02.02第2章 制作power bi图表 02.02.02.01 power pivot数据导入 00:08:43 02.02.02.02建立数据透视表 00:11: ...

  4. Buffer.h

    #ifndef __NOXIMBUFFER_H__ #define __NOXIMBUFFER_H__ #include <cassert> #include <queue> ...

  5. 工程无法正常调试运行unknown failure at android.os.Binder.execTransact

    同事正常使用的工程,放到另电脑上,开后可以正常编译,但是无法安装调试到手机上,始终提示错误 新建一个工程正常. 最后通过把开发工具升级到最新版本解决.

  6. 线程中的setDaemon方法

    setDaemon方法必须在start方法前定义.t1.setDaemon(True),该语句的意思是:将主线程A设置为子线程t1的守护线程.也就是在执行程序时,t1会随着主线程A的退出而退出,不论t ...

  7. [译]迁移到新的 React Context Api

    随着 React 16.3.0 的发布,context api 也有了很大的更新.我已经从旧版的 api 更新到了新版.这里就分享一下我(作者)的心得体会. 回顾 下面是一个展示如何使用旧版 api ...

  8. 命令更新emacs

    sudo apt-add-repository -y ppa:adrozdoff/emacs sudo apt update sudo apt install emacs25

  9. spring整合mybatis框架

    1.导入jar包 2.配置文件 a.    applicationContext.xml文件 <beans xmlns="http://www.springframework.org/ ...

  10. 把纯C的动态库代码改造成C++版的

    近期想把一份纯C的跨Win/Linux的动态库工程代码改成支持C++编译器,这样用C++写起代码来比较顺手.要点是保证动态库的ABI一致性,既导出接口不能改变. 主要的改动有: 1.把.c后缀名换成. ...