2023-12-18 11:11:17
Promise 是 JavaScript 中的一个重要概念,与前端的工作更是息息相关。因此本文将整理一下 Promise 在日常工作中的应用。
目录
从 MDN | 使用 Promise 中我们能学习到 Promise 的基础使用与错误处理、组合等概念,可以将 Promise
的特点概括为:
[[PromiseState]]
中:在现实工作中,当我们使用 Promise
时更多是对请求的管理,由于不同请求或任务的异步性。因此我们会根据不同的使用场景处理 Promise 的调度。
async/await
是基于 Promise
的一种语法糖,使得异步代码的编写和理解更加像同步代码。
当一个函数前通过 async 关键字声明,那这个函数的返回值一定会返回一个 Promise
,即便函数返回的值与 Promise
无关,也会隐式包装为 Promise
对象:
1 |
async function getAge() { |
await
操作符通常和 async
是配套使用的,它会等待 Promise
并拆开 Promise 包装直接取到里面的值。当它处于 await
状态时,Promise 还处于 ``,后续的代码将不会被执行,因此看起来像是 “同步” 的。
1 |
function delayResolve(x, timeout = 2000) { |
async/await
的错误处理通过是通过 try..catch
来捕获错误。当然,我们也会根据实际业务的需求来 catch 我们真正需要处理的问题。
1 |
try { |
学习了前文的基础概念后,我们可以更近一步的探讨 Promise 的使用。
Promise 串联一般是指多个 Promise 操作按顺序执行,其中每个操作的开始通常依赖于前一个操作的完成。这种串行执行的一个典型场景是在第一个异步操作完成后,其结果被用作第二个异步操作的输入,依此类推。
考虑以下场景:加载系统时,需要优先读取用户数据,同时需要用户的数据去读取用户的订单的信息,再需要两者信息生成用户报告。因此这是一个存在前后依赖的场景。
1 |
function fetchUserInfo(userId) { |
处理串联请求无非有两种方法:
方法 1: 链式调用 .then()
在这种方法中,我们利用 .then()
的链式调用来处理每个异步任务。这种方式的优点是每个步骤都明确且连贯,但可能导致所谓的“回调地狱”,尤其是在处理多个串联的异步操作时。
1 |
const userId = '12345'; // 假设已知的用户ID |
方法 2: 使用 async/await
async/await
提供了一种更加直观、类似同步的方式来处理异步操作。它使代码更易于阅读和维护,特别是在处理复杂的异步逻辑时。
1 |
async function getUserReport(userId) { |
在这个示例中,使用 async/await
使得代码的逻辑更加清晰和直观,减少了代码的嵌套深度,使错误处理变得简单。
以上是日常工作中最常见的需求.但这里我们还可以发散一下思维,考虑更复杂的情况:
现在有一个数组,数组内有 10 个或更多的异步函数,每个函数都依赖前一个异步函数的返回值需要做处理。在这种请求多了的特殊情况下我们手动维护会显得很冗余,因此可以通过循环来简化逻辑:
方法 1: 通过数组方法 reduce 组合
1 |
const processFunctions = [processStep1, processStep2, processStep3, ...]; |
方法 2: 循环体和 async/await 的结合
1 |
async function handleSequentialTasks(tasks, initialResult) { |
并发(Concurrency)在编程中是指多个任务在同一时间段内启动、执行,但不一定同时完成。在 JavaScript 的 Promise 中,并发通常涉及同时开始多个异步操作,并根据它们何时解决(fulfilled)或被拒绝(rejected)来进行相应的处理。
Promise 的并发会比串联的场景更复杂。Promise 对象提供了几个静态方法来处理并发情况,让开发者可以根据不同的使用场景选择合适的方法:
Promise.all(iterable)
Promise.all
静态方法接受一个 Promise 可迭代对象作为输入,当传入的数组中每个都被 resolve 后返回一个 Promise
。若任意一个 Promise 被 reject 后就 reject。
1 |
const promise1 = fetch('https://example.com/api/data1'); |
Promise.allSettled(iterable)
Promise.allSettled
方法同样接受一个 Promise 的可迭代对象。不同于 Promise.all
,这个方法等待所有传入的 Promise 都被解决(无论是 fulfilled 或 rejected),然后返回一个 Promise
,它解决为一个数组,每个数组元素代表对应的 Promise 的结果。这使得无论成功还是失败,你都可以得到每个 Promise 的结果。
1 |
Promise.allSettled([ |
Promise.race(iterable)
Promise.race
方法接受一个 Promise 的可迭代对象,但与 Promise.all
和 Promise.allSettled
不同,它不等待所有的 Promise 都被解决。相反,Promise.race
返回一个 Promise
,它解决或被拒绝取决于传入的迭代对象中哪个 Promise 最先解决或被拒绝。
1 |
function sleep(time, value, state) { |
Promise.any(iterable)
Promise.any
接受一个 Promise 的可迭代对象,并返回一个 Promise
。它解决为迭代对象中第一个被解决的 Promise 的结果。如果所有的 Promise 都被拒绝,Promise.any
会返回一个 AggregateError
实例。
1 |
const promise1 = new Promise((resolve, reject) => { |
JavaScript 默认提供的并发处理函数很方便我们根据业务场景的不同来处理请求,但显然我们工作中所遇到的需求得考虑更复杂的情况,还需要进一步的封装和扩展我们的 API。
在服务器端编程,我们经常遇到需要批量处理数据的场景。例如,批量修改数据库中的用户数据。在这种情况下,由于数据库操作的性能限制或者 API 调用限制,我们不能直接一口气修改全部,因为短时间内发出太多的请求数据库也会处理不来导致应用性能下降。因此,我们需要一种方法来限制同时进行的操作任务数,以保证程序的效率和稳定性。
我们代入实际业务场景:假设有一个社区组织了一次大型户外活动,活动吸引了大量参与者进行在线报名和付费。由于突发情况(比如恶劣天气或其他不可抗力因素),活动不得不取消。这时,组织者需要对所有已付费的参与者进行退款。
活动组织者发起「解散活动」后,服务端接收到请求后当然也不能一次性全部执行退款的操作啦,毕竟一场活动说不定有上千人。因此我们需要分批次去处理。
在上述社区活动退款的例子中,服务器端处理退款请求的一个有效方法是实施分批次并发控制。这种方法不仅保护了后端服务免受过载,还确保了整个退款过程的可管理性和可靠性。
分批次处理时有以下关键问题需要考虑:
将所有待处理的异步任务(如退款请求)存放在一个 tasks
数组中,在调用并发请求前将 tasks
数组分割成多个小批次,每个批次包含固定数量的请求。每当前一个批次处理完后,才处理下一个批次的请求,直到所有批次的请求都被处理完毕:
1 |
// 假设这些是返回 Promise 的函数 |
这种写法实现的并发简单易懂,也易于维护,在一些并发压力不大,比较简单的业务场景来看是足够了。
但如果我们将这种处理方式放在时序图上进行分析,就能发现服务器可能有能力处理更多的并发任务,而这种方法可能没有充分利用可用资源。每个批次开始前会依赖于上一个批次中请求响应时间最慢的那一个,因此我们还可以进一步考虑优化并发实现方案。
在之前的 “控制批次” 方法中,我们发现固定处理批次的局限性,尤其是在并发任务数量较大时可能导致的资源利用不足。为了解决这个问题,我们可以考虑采用一种更灵活的方法:维护一个动态的任务队列来处理异步请求:
我们封装一个函数,提供 concurrency
参数作为并发限制:
1 |
function parallelLimit(tasks, {concurrency = 10}) { |
该函数会在初始阶段会按照并发数先同步执行指定任务数,若某个任务执行完毕后,在执行完毕的回调中会唤醒下一个任务,直至任务队列执行完毕。
以下添加一些测试数据用于测试:
1 |
function asyncTask(id) { |
在实际的项目开发中,特别是面临复杂的并发处理需求时,我们更多会考虑使用成熟的第三库来处理业务问题,它们具备更完善的测试用例来检验边界情况。
处理并发的热门库有 RxJS
、p-map
和 async.js
。
RxJS
是一个以响应式编程为核心的库,竟然搭配 Angular
在网页端搭配使用,提供了丰富的操作符和方法来处理异步事件和数据流。p-map
和 async.js
包的体积更小,更适合在服务端中使用。p-map
专注于提供并发控制功能,而 async.js
提供包括并发控制、队列管理等广泛的异步处理模式,功能会更全。笔者在 Node.js
环境下只需要处理并发问题,故用的 p-map
会更多一些。下面简要介绍 p-map
的使用:
1 |
import pMap from 'p-map'; |
p-map
的源码实现很精简,建议想深入复习并发的同学去阅读其底层代码的实现作为参考思路。
在本文中,我们首先回顾了 Promise 的基本概念及其在 JavaScript 异步编程中的常用方法。通过这个基础,我们能够更好地理解如何有效地处理和组织异步代码。
随后,我们深入到并发处理的实际应用场景,探讨了如何根据具体需求选择合适的并发实现策略。我们讨论了从简单的批次控制到更复杂的动态任务队列的不同方法,展示了在不同场景下优化异步任务处理的多种可能性。
但值得注意的是,我们自行实现的并发控制工具在没有做足测试用例测试时,可能不适合直接应用于生产环境。在实际的项目开发中,选择成熟且持续维护的第三方库往往是更安全和高效的选择。比如笔者选择的 p-map
稳定性和可靠性相比上文简单实现的版本将会更好。
参考资料
2022-05-18 23:18:13
本篇将根据自考实践要求对「数据结构」一科进行简要的复习,代码实现使用 C++
语言实现。
已知 Q 是一个非空队列,S 是一个空栈。编写算法,仅用队列和栈的 ADT 函数和少量工作变量,将队列 Q 的所有元素逆置。
栈的基本 ADT 函数有:
void MakeEmpty(SqStack s);
void Push(SqStack s,ElemType e);
ElemType pop(SqStack s);
int isEmpty(SqStack s);
队列的基本ADT函数有:
题目要求:
1 |
// 栈的基本 ADT 函数有: |
基本思想: 每一趟在待排序的记录中选出关键字最小的记录,依次存放在已排好序的记录序列的最后,直到全部排序完为止。
1 |
|
基本思想: 每次将一个待排序的记录按其关键字的大小插入到前面已经排序好的文件中的适当位置,直到全部记录插入完位置。
1 |
void InsertSort(int arr[], int n) { |
冒泡排序的基本思想是:通过相邻元素之间的比较和交换,使娇小的元素逐渐从底部移向顶部,就像水底下气泡一样逐渐向上冒泡。
1 |
void BubbleSort(int *arr, int n) { |
2022-05-16 11:25:00
由于自考的实践考核要求有需要用到 mysql 进行考核,故记录一下在 mac 环境下试手的笔记。
首先在 mysql 官网中下载你想要的版本。可以直接下载 dmg 安装包,按照安装指示一步一步安装,并设置 mysql 的密码。
下载完毕后,一般情况下直接通过命令行使用 mysql
命令会找不到对应的命令:
1 |
➜ ~ mysql -v |
因此需要对当前的命令行工具配置对应的环境变量,比如笔者使用的是 zsh
,则打开 ~/.zshrc
文件添加以下配置:
1 |
export PATH=${PATH}:/usr/local/mysql/bin/ |
若使用 bash
的用户同理,直接在 ~/.bashrc
添加相同代码。添加完毕后通过 source
命令重新加载对应的环境变量: source ~/.zshrc
接着就可以在命令行直接使用 mysql
了。输入 mysql -u root -p
登录 mysql,密码是在安装阶段时设置的密码。
1 |
➜ ~ mysql -u root -p |
DATABASE 可以不区分大小写,但只能要么全小写,要么全大写。一般会将这些参数用大写写出。
1 |
-- 还可以通过 DEFAULT CHARACTER SET 选项设置默认的编码集 |
1 |
mysql> SHOW DATABASES; |
1 |
mysql> USE DANNY_DATABASE |
1 |
-- 创建数据库: 准备稍后移除的数据库 |
1 |
-- 未选择的情况下 |
1 |
-- 创建名为 customers 的数据表 |
其中 IF NOT EXISTS
参数是可选的,它的意思为若 customers 表不存在则创建它。
1 |
-- 查看当前用户在当前数据库中可以访问的数据表 |
1 |
-- 添加一个数据表用于演示删除 |
1 |
-- 插入新列 |
修改整列: 将列名 cust_sex 修改 sex,并修改默认值
1 |
mysql> alter TABLE customers |
仅修改列的类型
1 |
mysql> ALTER TABLE customers |
修改指定列的指定字段
1 |
mysql> ALTER TABLE customers |
移除数据表列: 移除 cust_contact 数据表项
1 |
mysql> ALTER TABLE danny_database.customers |
默认情况下在命令行中 mysql 是不能直接插入中文的,这个跟字符集有关。可输入下面命令修改数据库或表的字符集:
1 |
|
为数据表插入数据,显式设置字段
1 |
mysql> INSERT INTO danny_database.customers(cust_id, cust_name, sex, cust_address) |
由于 cust_id 是自增的,因此可以将此字段的值设置为 0 或 NULL 会自动自增。上例 “李四” 的 cust_id 在创建后就被自增为 902。
还可以通过 SET
语句设置部分值:
1 |
mysql> INSERT INTO danny_database.customers SET cust_name='王五', cust_address='武汉市', sex=DEFAULT; |
可通过 SELECT
语句查询数据:
1 |
mysql> SELECT * FROM customers; |
仅展示指定字段:
1 |
+---------+-----------+------+ |
通过 WHERE
子句设置查询条件,筛选出符合查询条件的数据:
1 |
mysql> SELECT cust_id,cust_name,cust_address FROM customers |
1 |
-- 添加几项测试数据 |
1 |
-- 更新数据 |
以一个 eShop 的需求为例做个简单的测试吧。
在 MySQL 中创建一个名为 eshop 的数据库,选择字符集为 utf8mb4
1 |
mysql> CREATE DATABASE IF NOT EXISTS eshop DEFAULT CHARACTER SET utf8mb4; |
相关表信息如下
表名:用户(t_user)
字段名 | 类型 | 大小 |
---|---|---|
用户ID (id) | 自增类型 | |
姓名 (user_name) | 文本 | 50,非空 |
联系电话 (phone_no) | 文本 | 20,非空 |
表名:商品(product)
字段名 | 类型 | 大小 |
---|---|---|
商品ID(id) | 自增类型 | |
商品名称(product_name) | 文本 | 50,非空 |
价格(price) | 数值类型 | (整数位9位,小数位2位),非空 |
表名:购物车 (shopping_cart)
字段名 | 类型 | 大小 |
---|---|---|
用户id(user_id) | 整数 | 非空,主键,参考用户表主键 |
商品id(product_id) | 整数 | 非空,主键,参考商品表主键 |
商品数量(quantity) | 整数 | 非空 |
1 |
-- 用户表 |
用户信息
1 |
1;张三; 13333333333; |
商品信息
1 |
1; C++程序设计教程; 45.5 |
购物车
1 |
1; 1; 5 |
录入数据:
1 |
-- 插入用户表数据 |
使用 SQL 语句列出「张三」购买商品清单信息,以购买数量升序排列:
1 |
mysql> SELECT u.user_name, p.product_name, u.phone_no, p.price, s.quantity FROM t_user u, product p, shopping_cart s |
使用 SQL 语句选出李四购买商品的总价:
1 |
mysql> SELECT u.user_name, p.product_name, p.price, s.quantity, p.price*s.quantity AS total_price FROM t_user u, product p, shopping_cart s |
使用 SQL 语句列出购买数量排前两位的商品名称:
1 |
mysql> SELECT p.product_name, p.price, s.quantity FROM product p, shopping_cart s |
若忘记数据库密码后可通过 mysqld_safe
来修改密码:
在系统偏好设置中关闭 mysql 服务
打开终端,输入命令:
1 |
➜ ~ cd /usr/local/mysql/bin |
命令行变成以 sh-3.2#
开头后继续输入命令:
1 |
sh-3.2# ./mysqld_safe --skip-grant-tables & |
新开个命令行窗口,进入 mysql
:
1 |
➜ ~ /usr/local/mysql/bin/mysql |
更新密码
1 |
mysql> update user set authentication_string=password('admin') where Host='localhost' and User='root'; |
输入 exit
命令退出 mysql
,查出 mysqld_safe
进程号并杀掉:
1 |
mysql> exit |
此时返回系统偏好设置中看到 mysql 被关闭后就算正确退出了。接着继续输入 mysql -u root -p
命令连接数据库,再输入刚才修改的密码即可。
参考资料
2022-01-22 10:40:49
在开发复杂的单页面应用时,我们经常会遇到一个问题:如何高效地在组件或模块之间进行通信?这里,EventBus
(事件总线)就派上了用场。简单来说,EventBus
是一种设计模式,它允许不同组件或模块之间通过事件来通信,而无需直接引用彼此。
EventBus
是传统的组件通信解决方案,下面我们将讲解 EventBus
跨组件通信的原理、实现方式以及该如何使用。
EventBus
的核心在于提供一个中央机制,允许不同的组件或模块相互通信,而不必直接引用对方。它是一种典型的发布-订阅(pub-sub)模式,这是一种广泛使用的设计模式,用于解耦发送者和接收者。
在这个模式中,EventBus 充当了一个中介的角色:它允许组件订阅那些它们感兴趣的事件,并在这些事件发生时接收通知。同样,当某个事件发生时,比如用户的一个动作或者数据的变化,EventBus 负责将这一消息广播给所有订阅了该事件的组件。
它基于三个核心操作:注册事件(on(event, callback)
)、触发事件(emit(event, ...args)
)、以及移除事件(off(event, callback)
)。因此,EventBus 的基本代码可以看到:
1 |
class EventBus { |
显然,我们需要有一个私有变量来储存用户的函数,此时为类添加 events
属性。events
属性是一个对象映射,其中每个属性表示一个事件名称,对应的值是一个回调函数的数组,这个数组存储了所有订阅了该事件的回调函数。
1 |
class EventBus { |
当用户执行订阅事件 on
时,回调函数会被添加到相应事件名称的数组中。这样,同一个事件可以被不同组件或模块订阅,而每个订阅者的回调函数都会被正确地保存在事件队列中。最后,当触发事件 emit
时,事件队列中的每个回调函数都会被执行,实现了事件的触发和通知功能。若已经没有订阅需求,则可以通过 off
移除已经订阅的事件。
接下来我们按照前文所述完善我们的代码实现:
1 |
class EventBus { |
当涉及到一次性的事件监听需求时,我们可以进一步扩展 EventBus,以支持一次性事件监听。允许用户在某个事件触发后,自动移除事件监听器,以确保回调函数只执行一次:
1 |
class EventBus { |
我们将类的封装到 event-bus.ts
中,通过模块的来管理:
1 |
export class EventBus { |
我们现在已经封装好了一个类,若我们像使用则需要实例化。此处再文件内直接实例化一个类:
1 |
// 创建 EventBus 实例并导出 |
这样使用时可以提供两种方式:
引入已经实例化的 eventBus
1 |
import { eventBus } from './event-bus'; |
需要多个独立的事件总线实例时,或者希望在不同模块或组件之间使用不同的事件总线时,可以选择额外实例化 eventBus。这样做的目的可能是为了隔离命名的冲突、组件与模块逻辑隔离等原因。
1 |
// events.ts |
1 |
import {eventBusA, eventBusB} from './events' |
以下是 CodeSandbox 的演示代码:
在本文中,我们深入探讨了 EventBus 的原理,了解了它是如何工作的。我们学习了它的核心操作。除了本文所提及的实现方式,有时候在生产项目中,为了确保代码的可靠性,我们可以考虑使用成熟的第三方库,例如 mitt 或 tiny-emitter。
这些库已经经过广泛的测试和使用,可以提供稳定和可靠的 EventBus 功能。
2021-10-11 12:08:31
Redux 是一个强大的状态管理框架,被广泛用于管理应用程序的状态。它的设计理念是让状态的更新可预测和透明。本文将简要探讨 Redux 的核心机制和实际应用。
在 Redux 中,有一个状态对象负责应用程序的整个状态.Redux store 是应用程序状态的唯一真实来源。
如果应用程序想要更新状态,只能通过 Redux store
执行,单向数据流可以更轻松地对应用程序中的状态进行监测管理。
Redux store 是一个保存和管理应用程序状态的 state,使用 Redux 对象中的 createStore()
来创建一个 redux store,此方法将 reducer 函数作为必需参数.
1 |
const reducer = (state = 5) => state; |
Redux store 对象提供了几种允许你与之交互的方法,可以使用 getState()
方法检索 Redux store 对象中保存的当前的 state
。
1 |
const store = Redux.createStore( |
由于 Redux 是一个状态管理框架,因此更新状态是其核心任务之一。在 Redux 中,所有状态更新都由 dispatch action 触发,action
只是一个 JavaScript 对象,其中包含有关已发生的 action
事件的信息。
Redux store 接收这些 action 对象,然后更新相应的状态。action
对象中必须要带有 type
属性,reducer 才能根据 type
进行区分处理。action
除了 type
属性外,还可以附带数据给 reducer 做相应的处理,这个数据是可选的。
我们可以将 Redux action 视为信使,将有关应用程序中发生的事件信息提供给 Redux store,然后 store 根据发生的 action 进行状态的更新。
reducer 将 state 和 action 作为参数,并且它总是返回一个新的 state。这是 reducer 的唯一的作用,它不应有任何其他的作用。比如它不应调用 API 接口,也不应存在任何潜在的副作用。reducer 只是一个接受状态和动作,然后返回新状态的纯函数。
在 reducer 中一般通过 switch 进行判断 action 的类型,做不同的处理。
store.subscribe()
可以订阅 store
的数据变化,它接收一个回调函数作为参数。当 store
数据更新时会调用该回调函数。
当应用程序的状态开始变得越来越复杂时,将状态划分为多个部分可能是个更好的选择。我们可以考虑将不同的模块进行划分,Login 作为一个模块,Account 作为另一个模块。
但对 state 进行模块划分也不能破坏 redux 中将数据存入简单 state 的原则。因此可以生成多个 reducer, 再将它们合并到 root reducer 中。
redux 提供了 combineReducers()
函数对 reducer 进行合并。它接收一个对象作为参数,对象中的 key/value 别分对应着 module name 和相对应的 reducer 函数。
1 |
|
redux 本身是不能直接处理异步操作,因此需要引入中间件来处理这些问题。在 createStore
时,还可以传入第二个可选参数,这个参数就是传递给 redux 的中间件函数。
Redux 提供了 applyMiddleware()
来创建一个中间件,一般处理 redux 异步的中间件有 redux-thunk
、redux-saga
等。
redux-thunk
允许 action 创建函数返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatch
和 getState
作为参数,允许直接进行异步操作和状态的分发。
例如,一个异步获取数据的 thunk
可能如下所示:
1 |
function fetchData() { |
redux-saga
是一个更高级的中间件,它使用 ES6 的 Generator
函数来让你以同步的方式写异步代码。saga 监听发起的 action,并决定基于这些 action 执行哪些副作用(如异步获取数据、访问浏览器缓存等)。
一个简单的 saga
可能如下所示:
1 |
function* fetchDataSaga(action) { |
在 React 应用中,Redux 被用来跨组件共享状态。使用 react-redux
库可以方便地将 Redux 集成到 React 应用中。
Provider
组件Provider
是 react-redux
提供的一个组件,它使 Redux store 对 React 应用中的所有组件可用。通常,我们在应用的最顶层包裹 Provider
并传入 store:
1 |
import { Provider } from 'react-redux'; |
connect
函数connect
是一个高阶函数,用于将 React 组件连接到 Redux store。它接受两个参数:mapStateToProps
和 mapDispatchToProps
,分别用于从 store 中读取状态和向 store 发起 actions。
1 |
import { connect } from 'react-redux'; |
Redux 提供了一种统一、可预测的方式来管理应用程序的状态。通过使用 actions, reducers 和 store,开发者可以以一种高度解耦的方式来管理状态和 UI。
当结合异步处理和 React 集成时,Redux 成为了一个强大的工具,能够提升大型应用程序的开发和维护效率。