当前位置: 首页 > news >正文

自己手写一个redux

提起 Redux 我们想到最多的应该就是 React-redux 这个库,可是实际上 Redux 和 React-redux 并不是同一个东西, Redux 是一种架构模式,源于 Flux。 React-redux 是 Redux 思想与 React 结合的一种具体实现。
在我们使用 React 的时候,常常会遇到组件深层次嵌套且需要值传递的情况,如果使用 props 进行值的传递,显然是非常痛苦的。为了解决这个问题,React 为我们提供了原生的 context API,但我们用的最多的解决方案却是使用 React-redux 这个基于 context API 封装的库。
本文并不介绍 React-redux 的具体用法,而是通过一个小例子,来了解下什么是 redux。

好了,现在我们言归正传,来实现我们自己的 redux。

一、最初

首先,我们用 creat-react-app 来创建一个项目,删除 src 下冗余部分,只保留 index.js,并修改 index.html 的 DOM 结构:

# index.html
<div id="root"><div id="head"></div><div id="body"></div>
</div>

我们在 index.js 中创建一个对象,用它来储存、管理我们整个应用的数据状态,并用渲染函数把数据渲染在页面:

const appState = {head: {text: '我是头部',color: 'red'},body: {text: '我是body',color: 'green'}
}function renderHead (state){const head = document.getElementById('head')head.innerText = state.head.text;head.style.color = state.head.color;
}
function renderBody (state){const body = document.getElementById('body')body.innerText = state.body.text;body.style.color = state.body.color;
}
function renderApp (state){renderHead(state);renderBody(state);
}
renderApp(appState);

此时运行代码,打开页面,我们可以看到,在 head 中已经出现了红色字体的‘我是头部’,在 body 中出现了绿色字体的‘我是body’。

如果我们把 head 和 body 看作是 root 中的两个组件,那么我们已经实现了一个全局唯一的 state 。这个 state 是全局共享的,随处可调用的。
我们可以修改 head 的渲染函数,来看下效果:

function renderHead (state){const head = document.getElementById('head')head.innerText = state.head.text + '--' + state.body.text;head.style.color = state.head.color;state.body.text = '我是经过 head 修改后的 body';
}

我们看到,在 head 渲染函数中,我们不仅可以取用 body 属性的值,还可以改变他的值。这样就存在一个严重的问题,因为 state 是全局共用的,一旦在一个地方改变了 state 的值,那么,所有用到这个值的组件都将受到影响,而且这个改变是不可预期的,显然给我们的代码调试增加了难度系数,这样的结果是我们不愿意看到的!

二、dispatch

现在看来,在我们面前出现了一个矛盾:我们需要数据共享,但共享数据被任意的修改又会造成不可预期的问题!
为了解决这个矛盾,我们需要一个管家,专门来管理共享数据的状态,任何对共享数据的操作都要通过他来完成,这样,就避免了随意修改共享数据带来的不可预期的危害!
我们重新定义一个函数,用这个函数充当我们的管家,来对我们的共享数据进行管理:

function dispatch(state, action) {switch (action.type) {case 'HEAD_COLOR':state.head.color = action.colorbreakcase 'BODY_TEXT':state.body.text = action.textbreakdefault:break}
}

我们来重新修改head 的渲染函数:

function renderHead (state){const head = document.getElementById('head')head.innerText = state.head.text + '--' + state.body.text;head.style.color = state.head.color;dispatch(state, { type: 'BODY_TEXT', text: '我是 head 经过调用 dispatch 修改后的 body' })
}

参考 前端进阶面试题详细解答

dispatch 函数接收两个参数,一个是需要修改的 state ,另一个是修改的值。这时,虽然我们依旧修改了 state ,但是通过 dispatch 函数,我们使这种改变变得可控,因为任何改变 state 的行为,我们都可以在 dispatch 中找到改变的源头。
这样,我们似乎已经解决了之前的矛盾,我们创建了一个全局的共享数据,而且严格的把控了任何改变这个数据的行为。
然而,在一个文件中,我们既要保存 state, 还要维护管家函数 dispatch,随着应用的越来越复杂,这个文件势必会变得冗长繁杂,难以维护。
现在,我们把 state 和 dispatch 单独抽离出来:

  • 用一个文件单独保存 state
  • 用另一个文件单独保存 dispatch 中修改 state 的对照关系 changeState
  • 最后再用一个文件,把他们结合起来,生成全局唯一的 store

这样,不仅使单个文件变得更加精简,而且在其他的应用中,我们也可以很方便的复用我们这套方法,只需要传入不同应用的 state 和修改 state 的对应逻辑 stateChange,就可以放心的通过调用 dispatch 方法,对数据进行各种操作了:

# 改变我们的目录结构,新增 redux 文件夹
+ src
++ redux
--- state.js // 储存应用数据状态
--- storeChange.js //  维护一套修改 store 的逻辑,只负责计算,返回新的 store
--- createStore.js // 结合 state 和 stateChange , 创建 store ,方便任何应用引用 
--index.js ## 修改后的各个文件# state.js -- 全局状态
export const state = {head: {text: '我是头部',color: 'red'},body: {text: '我是body',color: 'green'}
}# storeChange.js -- 只负责计算,修改 store
export const storeChange = (store, action) => {switch (action.type) {case 'HEAD_COLOR':store.head.color = action.colorbreakcase 'BODY_TEXT':store.body.text = action.textbreakdefault:break}
}# createStore.js -- 创建全局 store
export const createStore = (state, storeChange) => {const store = state || {};const dispatch = (action) => storeChange(store, action);return { store, dispatch }
}# index.js 
import { state } from './redux/state.js';
import { storeChange } from './redux/storeChange.js';
import { createStore } from './redux/createStore.js';
const { store, dispatch } = createStore(state, storeChange)function renderHead (state){const head = document.getElementById('head')head.innerText = state.text;head.style.color = state.color;
}
function renderBody (state){const body = document.getElementById('body')body.innerText = state.text;body.style.color = state.color;
}function renderApp (store){renderHead(store.head);renderBody(store.body);
}
// 首次渲染
renderApp(store);

通过以上的文件拆分,我们看到,不仅使单个文件更加精简,文件的职能也更加明确:

  • 在 state 中,我们只保存我们的共享数据
  • 在 storeChange 中,我们来维护改变 store 的对应逻辑,计算出新的 store
  • 在 createStore 中,我们创建 store
  • 在 index.js 中,我们只需要关心相应的业务逻辑

三、subscribe

一切似乎都那么美好,可是当我们在首次渲染后调用 dispatch 修改
store 时,我们发现,虽然数据被改变了,可是页面并没有刷新,只有在 dispatch 改变数据后,重新调用 renderApp() 才能实现页面的刷新。

// 首次渲染
renderApp(store);
dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' }) // 修改数据后,页面并没有自动刷新
renderApp(store);  // 重新调用 renderApp 页面刷新

这样,显然并不能达到我们的预期,我们并不想在每次改变数据后手动的刷新页面,如果能在改变数据后,自动进行页面的刷新,当然再好不过了!
如果直接把 renderApp 写在 dispatch 里,显然是不太合适的,这样我们的 createStore 就失去了通用性。
我们可以在 createStore 中新增一个收集数组,把 dispatch 调用后需要执行的方法统一收集起来,然后再循环执行,这样,就保证了 createStore 的通用性:

# createStore
export const createStore = (state, storeChange) => {const listeners = [];const store = state || {};const subscribe = (listen) => listeners.push(listen); const dispatch = (action) => {storeChange(store, action);listeners.forEach(item => {item(store);})};return { store, dispatch, subscribe }
}# index.js
···
const { store, dispatch, subscribe } = createStore(state, storeChange)
··· 
···
// 添加 listeners
subscribe((store) => renderApp(store));
renderApp(store);
dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });

这样,我们每次调用 dispatch 时,页面就会重新刷新。如果我们不想刷新页面,只想 alert 一句话,只需要更改添加的 listeners 就好了:

subscribe((store) => alert('页面刷新了'));
renderApp(store);
dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });

这样我们就保证了 createStore 的通用性。

四、优化

到这里,我们似乎已经实现了之前想达到的效果:我们实现了一个全局公用的 store , 而且这个 store 的修改是经过严格把控的,并且每次通过 dispatch 修改 store 后,都可以完成页面的自动刷新。
可是,显然这样并不足够,以上的代码仍有些简陋,存在严重的性能问题,

虽然我们只是修改了 body 的文案,可是,在页面重新渲染时,head 也被再次渲染。那么,我们是不是可以在页面渲染的时候,来对比新旧两个 store 来感知哪些部分需要重新渲染,哪些部分不必再次渲染呢?
根据上面的想法,我们再次来修改我们的代码:

# storeChange.js
export const storeChange = (store, action) => {switch (action.type) {case 'HEAD_COLOR':return { ...store,  head: { ...store.head, color: action.color }}case 'BODY_TEXT':return { ...store,body: {...store.body,text: action.text}}default:return { ...store }}
}# createStore.js
export const createStore = (state, storeChange) => {const listeners = [];let store = state || {};const subscribe = (listen) => listeners.push(listen);const dispatch = (action) => {const newStore = storeChange(store, action);listeners.forEach(item => {item(newStore, store);})store = newStore; };return { store, dispatch, subscribe }
}# index.js
import { state } from './redux/state.js';
import { storeChange } from './redux/storeChange.js';
import { createStore } from './redux/createStore.js';
const { store, dispatch, subscribe } = createStore(state, storeChange);function renderHead (state){console.log('render head');const head = document.getElementById('head')head.innerText = state.text;head.style.color = state.color;
}
function renderBody (state){console.log('render body');const body = document.getElementById('body')body.innerText = state.text;body.style.color = state.color;
}function renderApp (store, oldStore={}){if(store === oldStore) return; store.head !== oldStore.head && renderHead(store.head);  store.body !== oldStore.body && renderBody(store.body);  console.log('render app',store, oldStore);
}
// 首次渲染
subscribe((store, oldStore) => renderApp(store, oldStore));
renderApp(store);
dispatch({ type: 'BODY_TEXT', text: '我是调用 dispatch 修改的 body' });

以上,我们修改了 storeChange ,让他不再直接修改原来的 store,而是通过计算,返回一个新的 store 。我们又修改了 cearteStore 让他接收 storeChange 返回的新 store ,在 dispatch 修改数据并且页面刷新后,把新 store 赋值给之前的 store 。而在页面刷新时,我们来通过比较 newStore 和 oldStore ,感知需要重新渲染的部分,完成一些性能上的优化。

最后

我们通过简单的代码例子,简单了解下 redux,虽然代码仍有些简陋,可是我们已经实现了 redux 的几个核心理念:

  • 应用中的所有state都以一个object tree的形式存储在一个单一的store中。
  • 唯一能改store的方法是触发action,action是动作行为的抽象。

相关文章:

自己手写一个redux

提起 Redux 我们想到最多的应该就是 React-redux 这个库&#xff0c;可是实际上 Redux 和 React-redux 并不是同一个东西, Redux 是一种架构模式&#xff0c;源于 Flux。 React-redux 是 Redux 思想与 React 结合的一种具体实现。 在我们使用 React 的时候&#xff0c;常常会遇…...

mysql调优参数

my.conf [client] port 端口 socket sokcet位置 [mysqld] basedir mysql位置 port 3306 socket sokcet位置 datadir data目录 pid_file mysqld.pid位置 bind_address 0.0.0.0 lower_case…...

JavaEE简单示例——再插入的同时获取插入的主键列

简单介绍&#xff1a; 在某些时候&#xff0c;我们在插入完成一条语句之后&#xff0c;我们会想要返回之前插入的这条语句的主键列的数据&#xff0c;进行下一步的展示或者修改&#xff0c;我们就可以使用MyBatis的主键回写功能&#xff0c;帮助我们获取插入成功的一条数据的主…...

sql语句练习

一、现有以下两张表&#xff1a;第一张表名为cust&#xff0c;其表结构如下&#xff1a;第二张表名为mark&#xff0c;其表结构如下:1) [5分]请写出计算 所有学生的英语平均成绩的sq|语句。2) [5分]现有五 个学生,其学号假定分别为11,22,33,44,55;请用一条SQL语句实现列出这五个…...

广州蓝景—结合chatGPT下的教育模式变化

最近爆火的人工智能AI聊天工具ChatGPT&#xff0c;不仅在互联网&#xff0c;更是在各行各业中&#xff0c;得到了广泛的传播&#xff0c;应该没有哪一个不知道它的存在&#xff0c;但其实你又是否知道&#xff0c;其实ChatGPT是一类模型的统称&#xff0c;随着人工智能的快速发…...

大数据框架之Hadoop:MapReduce(三)MapReduce框架原理——shuffle机制

3.3.1Shuffle机制 Map方法之后&#xff0c;Reduce方法之前的数据处理过程称之为Shuffle。 3.3.2Partition分区 1、问题引出 要求将统计结果按照条件输出到不同文件中&#xff08;分区&#xff09;。比如&#xff1a;将统计结果按照手机归属地不同省份输出到不同文件中&#…...

4|无线传感器网络与应用|无线传感器网络原理及方法-许毅版|第3章:无线传感器网络通信-3.1协议结构 3.2物理层|青岛科技大学|课堂笔记

第3章&#xff1a;无线传感器网络通信3.1协议结构3.1.1 OSI参考模型1.网络通信协议MAC层和物理层采用IEEE 802.15.4协议*(1)物理层wsn物理层负责信号的调制和数据的收发&#xff0c;传输介质&#xff1a;无线电、红外线、光波等。(2)数据链路层wsn数据链路层负责数据成帧、帧检…...

关机时,如何控制systemd服务的关闭顺序

关机时&#xff0c;如何控制systemd服务的关闭顺序? 在工作中&#xff0c;我们通常遇到的问题是&#xff0c;如何控制systemd服务的启动顺序&#xff0c;同志们第一反应就会是使用Before或者After去进行控制。 问题来了&#xff0c;如果服务启动时没有顺序要求&#xff0c;但…...

关于MySQL镜像构建过程中添加自动初始化数据库

需求描述一般而言&#xff0c;我们在拉取了 mysql 镜像并运行之后&#xff0c;其中的并不会存在我们自定义的数据库&#xff0c;都是在镜像运行后&#xff0c;自己主动导入数据库&#xff0c;那么有没有方式可以一运行 mysql 镜像&#xff0c;对应生成的 mysql 容器中就有我们自…...

CS144-Lab2

实验架构 除了写入传入流之外&#xff0c;TCPReceiver 还负责通知 sender 两件事&#xff1a; “First unassembled” 字节的索引&#xff0c;称为“acknowledgment”或 “ackno”。这是接收方需要来自发送方的第一个字节。“first unassembled ” 索引和“first unacceptable…...

Linux内核驱动之efi-rtc

Linux内核驱动之efi-rtc1. UEFI与BIOS概述1.1. BIOS 概述1.1.1. BIOS缺点&#xff1a;1.1.2. BIOS的启动流程1.2 UEFI 概述1.2.1 Boot Sevices&#xff1a;1.2.2. Runtime Service&#xff1a;1.2.3. UEFI优点&#xff1a;1.2.4. UEFI启动过程&#xff1a;1.3 Legacy和UEFI1.4 …...

Java 字符串

文章目录一、API二、String1. String 构造方法2. String 对象的特点3. 字符串的比较4. 用户登录案例5. 遍历字符串6. 统计字符次数7. 拼接字符串8. 字符串反转三、StringBuilder1. 构造方法2. 添加及反转方法3. 与 String 相互转换4. 拼接字符串升级版5. 字符串反转升级版一、A…...

麦克风阵列波束基本概念理解

波束形成 本质上是设计合适的滤波器&#xff0c;对于一类固定滤波器系数的阵列来说&#xff0c;无论输入信号或者噪声信号的统计特征如何&#xff0c;其滤波器系数固定不变&#xff0c;此类波束形成叫Fixed Beamforming&#xff0c;固定波束形成好比传统数字信号处理里面的经典…...

JAVA保姆式JDBC数据库免费教程之02-连接池技术

连接池 连接池概念 ​ 概念&#xff1a;其实就是一个容器(集合)&#xff0c;存放数据库连接的容器。 当系统初始化好后&#xff0c;容器被创建&#xff0c;容器中会申请一些连接对象&#xff0c;当用户来访问数据库时&#xff0c;从容器中获取连接对象&#xff0c;用户访问完…...

视频片段怎么做成gif图?快试试这2种方法

动态gif图片作为当下非常常用的表情包&#xff0c;其丰富的内容生动的画面深受大众喜爱。那么&#xff0c;当我们想要将电影或是电视剧中的某一片段做成gif动态图片的时候&#xff0c;要如何操作呢&#xff1f;接下来&#xff0c;给大家分享两招视频转化gif的小窍门–使用【GIF…...

2.20计算机如何工作

一.计算机组成1.冯诺依曼体系CPU 中央处理器: 进行算术运算和逻辑判断.存储器: 分为外存和内存, 用于存储数据(使用二进制方式存储)输入设备: 用户给计算机发号施令的设备.输出设备: 计算机个用户汇报结果的设备内存和外存的区别(面试)访问速度:内存快,外存慢存储空间:内存小,外…...

[golang gin框架] 5.Cookie以及Session

1.Cookie(1).介绍HTTP 是无状态协议,简单地说&#xff0c;当浏览了一个页面&#xff0c;然后转到同一个网站的另一个页面&#xff0c;服务器无法认识到这是同一个浏览器在访问同一个网站,每一次的访问&#xff0c;都是没有任何关系的,如果要实现多个页面之间共享数据的话就可以…...

【牛客刷题专栏】0x0B:JZ3 数组中重复的数字(C语言编程题)

前言 个人推荐在牛客网刷题(点击可以跳转)&#xff0c;它登陆后会保存刷题记录进度&#xff0c;重新登录时写过的题目代码不会丢失。个人刷题练习系列专栏&#xff1a;个人CSDN牛客刷题专栏。 题目来自&#xff1a;牛客/题库 / 在线编程 / 剑指offer&#xff1a; 目录前言问题…...

js中的隐式类型转换有哪些

目录一、隐式类型转换条件二、 的隐式类型转换三、 的隐式类型转换四、object 的隐式类型转换探讨 object 的隐式转换执行顺序探讨 Symbol.toPrimitive 属性如何将对象转换为原始值在前端js这门动态弱类型语言中&#xff0c;不仅存在着显示类型转换&#xff0c;还存在许多隐式类…...

WuThreat身份安全云-TVD每日漏洞情报-2023-02-17

漏洞名称:IBM Aspera Faspex 预身份验证 RCE 漏洞 漏洞级别:高危 漏洞编号:CVE-2022-47986 相关涉及:IBM Aspera Faspex 漏洞状态:POC 参考链接:https://tvd.wuthreat.com/#/listDetail?TVD_IDTVD-2023-02805 漏洞名称:Kardex Mlog MCC PATH 遍历 漏洞级别:严重 漏洞编号:CVE…...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

基于算法竞赛的c++编程(28)结构体的进阶应用

结构体的嵌套与复杂数据组织 在C中&#xff0c;结构体可以嵌套使用&#xff0c;形成更复杂的数据结构。例如&#xff0c;可以通过嵌套结构体描述多层级数据关系&#xff1a; struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

内存分配函数malloc kmalloc vmalloc

内存分配函数malloc kmalloc vmalloc malloc实现步骤: 1)请求大小调整:首先,malloc 需要调整用户请求的大小,以适应内部数据结构(例如,可能需要存储额外的元数据)。通常,这包括对齐调整,确保分配的内存地址满足特定硬件要求(如对齐到8字节或16字节边界)。 2)空闲…...

盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来

一、破局&#xff1a;PCB行业的时代之问 在数字经济蓬勃发展的浪潮中&#xff0c;PCB&#xff08;印制电路板&#xff09;作为 “电子产品之母”&#xff0c;其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透&#xff0c;PCB行业面临着前所未有的挑战与机遇。产品迭代…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

三维GIS开发cesium智慧地铁教程(5)Cesium相机控制

一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点&#xff1a; 路径验证&#xff1a;确保相对路径.…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)

文章目录 1.什么是Redis&#xff1f;2.为什么要使用redis作为mysql的缓存&#xff1f;3.什么是缓存雪崩、缓存穿透、缓存击穿&#xff1f;3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务

通过akshare库&#xff0c;获取股票数据&#xff0c;并生成TabPFN这个模型 可以识别、处理的格式&#xff0c;写一个完整的预处理示例&#xff0c;并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务&#xff0c;进行预测并输…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍

文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结&#xff1a; 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析&#xff1a; 实际业务去理解体会统一注…...

聊一聊接口测试的意义有哪些?

目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开&#xff0c;首…...