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)就是:

{
milk: 0
}

Actions

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

{
type: "PUT_MILK",
count: 1
}

Reducers

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

const initState = {
milk: 0
} function reducer(state = initState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count}
default:
return state
}
}

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

简单例子

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

import { createStore } from 'redux';

const initState = {
milk: 0
}; function reducer(state = initState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count};
case 'TAKE_MILK':
return {...state, milk: state.milk - action.count};
default:
return state;
}
} let store = createStore(reducer); // subscribe其实就是订阅store的变化,一旦store发生了变化,传入的回调函数就会被调用
// 如果是结合页面更新,更新的操作就是在这里执行
store.subscribe(() => console.log(store.getState())); // 将action发出去要用dispatch
store.dispatch({ type: 'PUT_MILK' }); // milk: 1
store.dispatch({ type: 'PUT_MILK' }); // milk: 2
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触发回调,想到了什么,这不就是发布订阅模式吗?我之前有一篇文章详细讲过发布订阅模式了,这里直接仿写一个。

function createStore() {
let state; // state记录所有状态
let listeners = []; // 保存所有注册的回调 function subscribe(callback) {
listeners.push(callback); // subscribe就是将回调保存下来
} // dispatch就是将所有的回调拿出来依次执行就行
function dispatch() {
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
} // getState直接返回state
function getState() {
return state;
} // store包装一下前面的方法直接返回
const store = {
subscribe,
dispatch,
getState
} return store;
}

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

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

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

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

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

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

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

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

手写combineReducers

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

import { createStore, combineReducers } from 'redux';

const initMilkState = {
milk: 0
};
function milkReducer(state = initMilkState, action) {
switch (action.type) {
case 'PUT_MILK':
return {...state, milk: state.milk + action.count};
case 'TAKE_MILK':
return {...state, milk: state.milk - action.count};
default:
return state;
}
} const initRiceState = {
rice: 0
};
function riceReducer(state = initRiceState, action) {
switch (action.type) {
case 'PUT_RICE':
return {...state, rice: state.rice + action.count};
case 'TAKE_RICE':
return {...state, rice: state.rice - action.count};
default:
return state;
}
} // 使用combineReducers组合两个reducer
const reducer = combineReducers({milkState: milkReducer, riceState: riceReducer}); let store = createStore(reducer); store.subscribe(() => console.log(store.getState())); // 操作

手写一个Redux,深入理解其原理的更多相关文章

  1. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  2. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  3. 一起学习造轮子(二):从零开始写一个Redux

    本文是一起学习造轮子系列的第二篇,本篇我们将从零开始写一个小巧完整的Redux,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Promises/A+,Red ...

  4. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

  5. webview的简单介绍和手写一个H5套壳的webview

    1.webview是什么?作用是什么?和浏览器有什么关系? Webview 是一个基于webkit引擎,可以解析DOM 元素,展示html页面的控件,它和浏览器展示页面的原理是相同的,所以可以把它当做 ...

  6. 摊牌了!我要手写一个“Spring Boot”

    目前的话,已经把 Spring MVC 相关常用的注解比如@GetMapping .@PostMapping .@PathVariable 写完了.我也已经将项目开源出来了,地址:https://gi ...

  7. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  8. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  9. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

随机推荐

  1. java实现第四届蓝桥杯埃及分数

    埃及分数 古埃及曾经创造出灿烂的人类文明,他们的分数表示却很令人不解.古埃及喜欢把一个分数分解为类似: 1/a + 1/b 的格式. 这里,a 和 b 必须是不同的两个整数,分子必须为 1 比如,2/ ...

  2. java实现第六届蓝桥杯无穷分数

    无穷分数 无穷分数 无穷的分数,有时会趋向于固定的数字. 请计算[图1.jpg]所示的无穷分数,要求四舍五入,精确到小数点后5位,小数位不足的补0. 请填写该浮点数,不能填写任何多余的内容. 结果:0 ...

  3. Linux fdisk手动分区

    查询系统中可以被识别的硬盘:fdisk -l 可以看到,我的服务器只有一块硬盘,容量是53.7GB,有1个分区,文件系统类型是Linux,Id 是83(82是Linux swap分区,相当于Windo ...

  4. ReentrantReadWriteLock源码分析及理解

    本文结构 读写锁简介:介绍读写锁.读写锁的特性以及类定义信息 公平策略及Sync同步器:介绍读写锁提供的公平策略以及同步器源码分析 读锁:介绍读锁的一些常用操作和读锁的加锁.解锁的源码分析 写锁:介绍 ...

  5. zabbix 磁盘分区监控

    系统环境 Zabbix 版本:3.4 操作系统版本:centos7.4 监控分区 / ./boot ./home   先创建监控项的模板 点击创建监控项 名称随意填写,键值的话因为我们监控车的是磁盘剩 ...

  6. TypeScript使用体会(一)

    typescript使用体会 近期接手了一个公司项目是由TS写的,第一次用在这里做一下简单的使用体会 个人觉得TS与JS相差不多,只是多了一些约束(可能自己还没体会到精髓) typescript是Ja ...

  7. 面试官:换人!他连 TCP 这几个参数都不懂

    每日一句英语学习,每天进步一点点: 前言 TCP 性能的提升不仅考察 TCP 的理论知识,还考察了对于操心系统提供的内核参数的理解与应用. TCP 协议是由操作系统实现,所以操作系统提供了不少调节 T ...

  8. 树莓派学习--安装GPIO Zero

    GPIO ZERO 在Raspbian的映像内默认安装,对于其他系统 先更新存储库列表: sudo apt update 对于python3: sudo apt install python3-gpi ...

  9. Nice Jquery Validator 内置属性

    required - 必填 适用于 input.textarea.select 输入框.(checkbox 与 radio 请使用 checked 规则)字段必填,则值不能为空.字段非必填,则值为空的 ...

  10. 记录一次更改服务器名称导致mysql 不能正常登录、启动

    由于客户要求更改服务器的名称,以便区分多台服务器:修改前mysql 能正常登录,但是修改后,登录时报错: Enter password: ERROR 1524 (HY000): Plugin '*C6 ...