使用promise创建一个同步事件驱动api

使用promise创建一个同步事件驱动api

事件驱动体系结构(EDA)是一种强大的方法,可以在网络上构建松散耦合、性能好、可伸缩的应用程序。它支持非常多功能,如推送通知、协作编辑和多人操作,以及其他实时交互。

但有时模型与我们开发人员需要的不一致。当两个应用程序层只能通过异步消息传递进行通信时,我们就不得不重新修改代码。

在本文中,将使用Promise实现一个通用的事件驱动API,它隐藏了消息传递的复杂性和模板,并允许我们跨应用程序编写线性代码。

Request/Response vs EDA

传统的网络应用程序处理跨边界的通信通常都是使用HTTP这种Request/Response模式。该模型的特点是请求者发送消息,然后等待响应,接收、处理和响应消息。尽管这可能发生在异步,但是我们可以称此模型为"同步"。

在这里插入图片描述
EDA中也被称为 发行/订阅模式,请求和接收数据的过程是独立的,以非阻塞、异步的方式进行。通常,客户端会订阅来自服务器的消息,服务器会订阅来自客户端的消息。当客户端请求数据时,它只是发出信息,然后继续执行。服务器接收消息,处理它,并在某个时候向客户端发送另一条消息。客户端作为订阅者,从其原始请求中接收消息并处理它认为合适的消息。但是使用不同的网络请求,甚至其他协议,这可能会在另一个时间发生。

在这里插入图片描述

事件驱动模型此时就出现了一些关键优势。首先,EDA允许客户端在服务器上的事件响应时进行通知。这消除了昂贵的轮询,并支持对来自别处的通知和事件进行推送。其次,它可以通过允许消息处理与消息发送分离来鼓励较少耦合的代码。第三,它赋予开发人员并行化处理和构建弹性系统的能力。第四,它使系统具有固有的容错性,因为只有被识别的消息才会被订阅者处理。

EDA的劣势

在复杂的应用程序中,事件驱动的架构可以提供大量的优势。但有时候我们只需要在特定的执行上下文中立即获取数据。或者我们只是想把远程资源当作本地资源来处理。

假设我们有一个应用程序,它必须对用户输入执行一些复杂的计算。我们最好使用web worker,它使用一个单独的线程进行工作。

// Create a Web Worker
const worker = new Worker("worker.js");

const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
  worker.postMessage({ type: "expensiveComputation", payload: 42 });
});

worker.addEventListener("message", (event) => {
  const { type, payload } = event.data;
  switch(type) {
    case "expensiveComputation":
      doSomethingWithResult(payload);
      break;
      
    default:
      break;
  }
});

我们的work.js模块监听来自主线程的消息,执行昂贵的计算,并向主线程发送响应:

self.onmessage = async (event) => {
  const { type, payload } = event.data;
  switch(type) {
    case "expensiveComputation":
      const result = await doExpensiveComputation(payload);
      postMessage({ type, payload: result });
      break;

    default:
      break;
  }
}

客户端可以发送计算请求,然后收到结果,执行doSomethingWithResult方法。但这个解决方案有个限制,我们不能在同一个地方请求数据并使用响应的数据 。

实现一个Sync Bridge

为了以"同步"的方式消耗事件流,我们需要将其接口转换成一个使用Promise的接口–Sync Bridge

  1. 在客户端发送消息之前,给消息带上id 或者是唯一标识,用于匹对响应消息。
  2. 初始化一个Promise
  3. 把这个Promise 返回给请求者,发送消息。
  4. 在主机订阅客户端消息。
  5. 订阅在客户端上的主机消息。当收到一条载有id消息时,检查是否是pending状态。如果是,那么从数据结构中弹出它,并决定或拒绝承诺。这里的"客户端"和"主机"可以是参与消息传递的任何实体。有时客户端也可以充当主机,反之亦然,因此我们也可以根据这些实体的使用上下文将其称为"请求者"和"响应者"。

通过在客户端和主机之间创建一个协议,同意用一个共同的、唯一的ID标记请求和相应的响应消息,使响应能够"路由"到正确的请求者,我们就可以规避EDA的限制。我们在请求端留下一个作为占位符的Promise,一旦收到等待的消息,我们就用真实数据来填充。

要将此应用于我们之前的web worker代码,让我们编写一些助手类来抽象上面列出的流程。我们需要某种客户端抽象来为消息分配ID,跟踪挂起的请求,并监听响应:

const DEFAULT_CHANNEL = "__worker_channel";

export class WorkerClient {
  #channel;
  #worker;
  #receiver;
  #pending = new Map();

  constructor(workerUrl, channel = DEFAULT_CHANNEL) {

    this.#channel = channel;
    this.#worker = new Worker(workerUrl, { type: "module" });
    this.#receiver = (event) => {
      if (!event.data || !event.data.id) return;
      if (event.data.channel !== this.#channel) return;
      if (!this.#pending.has(event.data.id)) return;
      const [resolve, reject] = this.#pending.get(event.data.id);
      if ("payload" in event.data) {
        resolve(event.data.payload);
      } else if ("error" in event.data) {
        reject(event.data.error);
      } else {
        reject(new Error("Malformed response"));
      }
      this.#pending.delete(event.data.id);
    };

    this.#worker.addEventListener("message", this.#receiver);
  }

  async post(payload) {
    const id = Math.floor(Math.random() * 1_000_000).toString(16);
    return new Promise((resolve, reject) => {
      this.#pending.set(id, [resolve, reject]);
      
      // Dispatch a message to the worker
      this.#worker.postMessage({
        id,
        channel: this.#channel,
        payload,
      });
    });
  }
}

在主机方面,我们需要一个控制器,它过滤掉我们不感兴趣的消息,执行一些单元的工作,并将消息发送到请求者的"返回地址":

 export class WorkerHost {
  #channel;
  #receivers = new Map();

  constructor(channel = DEFAULT_CHANNEL) {
    this.#channel = channel;
  }

  on(type = "message", callback) {
    const wrapper = async (event) => {
      // Filter out irrelevant messages
      if (!event.data || !event.data.id) return;
      if (event.data.channel !== this.#channel) return;
     
      try {
        const payload = await callback(event);
        postMessage({
          id: event.data.id,
          channel: this.#channel,
          payload,
        });
      } catch (error) {
        postMessage({
          id: event.data.id,
          channel: this.#channel,
          error,
        });
      } 
    };

    this.#receivers.set(callback, wrapper);
    addEventListener(type, wrapper);
  }

  off(type = "message", callback) {
    const wrapper = this.#receivers.get(callback);
    if (wrapper) {
      removeEventListener(type, wrapper);
      this.#receivers.delete(callback);
    }
  }
}

使用这些辅助程序,我们将改写之前应用程序代码,以使用基于Promise的新·API·。注意我们现在可以直接在我们的点击处理程序中的处理数据:

const client = new WorkerClient("worker.js");

const btn = document.getElementById("btn");
btn.addEventListener("click", async () => {
  const data = await client.post({ type: "expensiveComputation", payload: 42 });
  doSomethingWithResult(data);
});

我们的处理程序现在只是返回值,而不是发布消息(消息传递是为我们处理的):

const host = new WorkerHost();

host.on("message", async (event) => {
  const { type, payload } = event.data;
  switch(type) {
    case "expensiveComputation":
      const result = await doExpensiveComputation(payload);
      return result;

    default:
      break;
  }
});

我们已经写了相当数量的代码。我们到底得到了什么?

Sync Bridge适配器作为一个真正的Promise,实质上改变了预期将来会收到一些信息。它允许我们在远程上下文中处理数据和代码,就像它是本地的一样。最重要的是,它允许我们在同一地点请求和使用远程数据。

我们现在还可以将不同类型的消息限制在离散通道上,使消息处理特定、快速和本地化的代码只限于需要的地方。如果我们想的话,多重的WorkerClient甚至可以共享同一个通道。

这种模式可以很容易地推广到大多数事件驱动的系统。我们可以修改我们示例中的方法,以接收更多EventTarget ,为任何消息流提供通用的同步接口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/125246.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【树的存储结构,孩子链表】

文章目录 树和森林树的存储结构孩子链表 树和森林 森林:是m(m>0)棵互不相交的树的集合。 树的存储结构 1.双亲表示法 实现:定义结构数组存放树的结点,每个结点含两个域。 数据域:存放结点本身信息。 双亲域:指…

虚假内容检测,谣言检测,不实信息检测,事实核查;纯文本,多模态,多语言;数据集整理

本博客系博主个人理解和整理所得,包含内容无法详尽,如有补充,欢迎讨论。 这里只提供数据集相关介绍和来源出处,或者下载地址等,因版权原因不提供数据集所含的元数据。如有需要,请自行下载。 “Complete d…

亚马逊云科技海外服务器初体验

目录 前言亚马逊云科技海外服务器概述注册使用流程实例创建性能表现用户体验服务支持初体验总结 前言 随着云原生技术的飞速发展,越来越多的企业和开发者选择云服务器来作为自己的使用工具,云原生技术的发展也促进了云服务厂商的产品发展,所…

Leetcode Hot 100之四:283. 移动零+11. 盛最多水的容器

283.移动零 题目: 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。 示例 1: 输入: nums [0,1,0,3,12] 输出: [1,3,12,0,0] …

算法通关村第七关-黄金挑战二叉树迭代遍历

大家好我是苏麟 , 今天带来二叉树的迭代遍历 . 二叉树的迭代遍历 前序编列 描述 : 给你二叉树的根节点 root ,返回它节点值的 前序 遍历。 题目 : LeetCode 二叉树的前序遍历 : 144. 二叉树的前序遍历 分析 : 前序遍历是中左右,如果还有左子树就一…

一款基于.Net开发、开源、支持多平台云存储文件管理器

目录 01 项目简介02 项目代码03 部分截图04 项目地址 今天给大家推荐一款基于基于.Net开发、开源的,支持多平台的云存储文件管理器。 01 项目简介 Camelotia是一款云存储文件管理器,基于.Net UI框架和ReactiveUI框架开发的,目前支持的平台有…

AI:75-基于生成对抗网络的虚拟现实场景增强

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…

Bitget Wallet:使用 Base 链购买 ETH 的简明教程

Base 链是一种 Layer 2(L2)公链,它可以为用户提供以太坊(ETH)代币,而 Bitget Wallet 是一款多功能加密货币钱包,支持 Base 链以及其他主要区块链。

进行 “最佳价格查询器” 的开发

前置条件 public class Shop {private final String name;private final Random random;public Shop(String name) {this.name name;random new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));}public double getPrice(String product) {return calculatePrice…

SpringBoot自动配置的原理篇,剖析自动配置原理;实现自定义启动类!附有代码及截图详细讲解

SpringBoot 自动配置 Condition Condition 是在Spring 4.0 增加的条件判断功能,通过这个可以功能可以实现选择性的创建 Bean 操作 思考:SpringBoot是如何知道要创建哪个Bean的?比如SpringBoot是如何知道要创建RedisTemplate的?…

剖析WPF模板机制的内部实现

剖析WPF模板机制的内部实现 众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElement、FrameworkElment、Control等)都直接或间接继承自Visual类。…

单例模式 rust和java的实现

文章目录 单例模式介绍应用实例:优点使用场景 架构图JAVA 实现单例模式的几种实现方式 rust实现 rust代码仓库 单例模式 单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建…

【第2章 Node.js基础】2.4 Node.js 全局对象...持续更新

什么是Node.js 全局对象 对于浏览器引擎来说,JavaScript 脚本中的 window 是全局对象,而Node.js程序中的全局对象是 global,所有全局变量(除global本身外)都是global 对象的属性。全局变量和全局对象是所有模块都可以调用的。Node.is 的全局…

docker部署mongodb

1:拉去momgodb镜像 2:拉去成功后,通过docker-compose.yml配置文件启动mongodb,docker-compose.yml配置如下 version: 3.8 services:mongodb-1:container_name: mongodbimage: mongo ports:- "27017:27017"volumes:- G:…

TCP/IP详解

TCP/IP详解 一、网络基础1.TCP/IP网络分层2.IP地址和端口号3.封装与分用4.客户-服务端模型 二、链路层1.以太网IEEE802封装2.环回接口 Loopback Interface3.最大传输单元MTU和路径MTU 三、网络层 - IP1.IP首部的关键信息2.IP路由选择3.子网寻址和子网掩码4.ICMP和IGMP 四、传输…

python爬虫hook定位技巧、反调试技巧、常用辅助工具

一、浏览器调试面板介绍 二、hook定位、反调试 Hook 是一种钩子技术,在系统没有调用函数之前,钩子程序就先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,也可以强制结束消息的传递。简单…

Postgresql数据类型-布尔类型

前面介绍了PostgreSQL支持的数字类型、字符类型、时间日期类型,这些数据类型是关系型数据库的常规数据类型,此外PostgreSQL还支持很多非常规数据类型,比如布尔类型、网络地址类型、数组类型、范围类型、json/jsonb类型等,从这一节…

python poetry的教程

Poetry Python世界中,Poetry是一个近年来备受瞩目的工具,它为开发者提供了一个灵活且强大的依赖管理解决方案。Poetry可以帮助开发者管理项目的依赖关系,同时提供了一系列的工具和功能,使开发者能够更轻松地创建和管理复杂的项目。…