当前位置: 首页 > 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…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查

在对接支付宝API的时候&#xff0c;遇到了一些问题&#xff0c;记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

【力扣数据库知识手册笔记】索引

索引 索引的优缺点 优点1. 通过创建唯一性索引&#xff0c;可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度&#xff08;创建索引的主要原因&#xff09;。3. 可以加速表和表之间的连接&#xff0c;实现数据的参考完整性。4. 可以在查询过程中&#xff0c;…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

技术栈RabbitMq的介绍和使用

目录 1. 什么是消息队列&#xff1f;2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...

go 里面的指针

指针 在 Go 中&#xff0c;指针&#xff08;pointer&#xff09;是一个变量的内存地址&#xff0c;就像 C 语言那样&#xff1a; a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10&#xff0c;通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...

【UE5 C++】通过文件对话框获取选择文件的路径

目录 效果 步骤 源码 效果 步骤 1. 在“xxx.Build.cs”中添加需要使用的模块 &#xff0c;这里主要使用“DesktopPlatform”模块 2. 添加后闭UE编辑器&#xff0c;右键点击 .uproject 文件&#xff0c;选择 "Generate Visual Studio project files"&#xff0c;重…...

面试高频问题

文章目录 &#x1f680; 消息队列核心技术揭秘&#xff1a;从入门到秒杀面试官1️⃣ Kafka为何能"吞云吐雾"&#xff1f;性能背后的秘密1.1 顺序写入与零拷贝&#xff1a;性能的双引擎1.2 分区并行&#xff1a;数据的"八车道高速公路"1.3 页缓存与批量处理…...