异步单例模式之不一样的单例模式

开发 前端
创建实例需要一定的时间,创建期间,交出执行权,创建完毕后,拿回执行权,返回结果。

[[422320]]

本文转载自微信公众号「云的程序世界」,作者云的世界。转载本文请联系云的程序世界公众号。

前言

单例模式大家都知道,异步单例又为何物。

异步单例:

创建实例需要一定的时间,创建期间,交出执行权,创建完毕后,拿回执行权,返回结果。

有人可能会吐槽,就这,其他方案分分钟搞定。没错,没有谁不可被替代。

这里主要表达的是一种编程思想,其能改变代码风格, 特定情况下漂亮的解决问题。多一种手段,多一种选择。

先一起来看一个栗子:

asyncInsCreator延时2秒创建一个对象;

getAsyncIns 封装异步对象获取过程;

我们多次调用 getAsyncIns, 得到同一个对象。

  1. async function asyncInsCreator() { 
  2.     await delay(2000).run(); 
  3.     return new Object(); 
  4.  
  5. function getAsyncIns() { 
  6.     return factory(asyncInsCreator); 
  7.  
  8. ; (async function test() { 
  9.     try {   
  10.         const [ins1, ins2, ins3] = await Promise.all([ 
  11.             getAsyncIns(), 
  12.             getAsyncIns(), 
  13.             getAsyncIns() 
  14.         ]); 
  15.  
  16.         console.log("ins1:", ins1);  // ins1: {} 
  17.         console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true 
  18.         console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true 
  19.         console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true 
  20.     } catch (err) { 
  21.         console.log("err", err); 
  22.     } 
  23. })(); 

适用场景

异步单例

比如初始化socket.io客户端, indexedDB等等

仅仅一次的情况

举一个例子,我们可以注册多个 load事件

  1. window.addEventListener("load"function () { 
  2.      // other code 
  3.      console.log("load 1"); 
  4. }); 
  5.  
  6. window.addEventListener("load"function () { 
  7.      // other code 
  8.      console.log("load 2"); 
  9. ); 

这要是换做React或者Vue,你先得订阅还得取消订阅,显得麻烦,当然你可以利用订阅发布思想再包装一层:

如果换成如下,是不是赏心悦目:

  1. await loaded(); 
  2. // TODO::   

你肯定说,这个我会:

  1. function loaded() { 
  2.        return new Promise((resove, reject) => { 
  3.            window.addEventListener("load", resove) 
  4.        }); 
  5.    } 

我给你一段测试代码:

下面只会输出 loaded 1,不会输出loaded 2。

至于原因:load事件只会触发一次。

  1. function loaded() { 
  2.     return new Promise((resolve, reject) => { 
  3.         window.addEventListener("load", ()=> resolve(null)); 
  4.     }); 
  5.  
  6. async function test() { 
  7.     await loaded(); 
  8.     console.log("loaded 1"); 
  9.      
  10.     setTimeout(async () => { 
  11.         await loaded(); 
  12.         console.log("loaded 2"); 
  13.     }, 1000) 
  14.  
  15. est(); 

到这里,我们的异步单例就可以秀一把,虽然他本意不是干这个,但他可以,因为他满足仅仅一次的条件。

我们看看使用异步单例模式的代码:

loaded 1 与 loaded 2 都如期到来。

  1. const factory = asyncFactory(); 
  2.  
  3. function asyncInsCreator() { 
  4.     return new Promise((resove, reject) => { 
  5.         window.addEventListener("load", ) 
  6.     }); 
  7.  
  8. function loaded() { 
  9.     return factory(asyncInsCreator) 
  10.  
  11. async function test() { 
  12.     await loaded(); 
  13.     console.log("loaded 1");  // loaded 1 
  14.  
  15.     setTimeout(async () => { 
  16.         await loaded(); 
  17.         console.log("loaded 2"); // loaded 2 
  18.     }, 1000) 
  19.  
  20. test(); 

实现思路

状态

实例创建,其实也就只有简简单单的两种状态:

  1. 创建中
  2. 创建完毕

难点在于,创建中的时候,又有新的请求来获取实例。

那么我们就需要一个队列或者数组来维护这些请求队列,等待实例创建完毕,再通知请求方。

如果实例化已经完毕,那么之后就直接返回实例就好了。

变量

我们这里就需要三个变量:

  1. instance 存储已经创建完毕的实例
  2. initializing 是否创建中
  3. requests 来保存哪些处于创建中,发过来的请求

工具方法

delay:

延时一定时间调用指定的函数。

用于后面的超时,和模拟延时。

  1. export function delay(delay: number = 5000, fn = () => { }, context = null) { 
  2.     let ticket = null
  3.     return { 
  4.         run(...args: any[]) { 
  5.             return new Promise((resolve, reject) => { 
  6.                 ticket = setTimeout(async () => { 
  7.                     try { 
  8.                         const res = await fn.apply(context, args); 
  9.                         resolve(res); 
  10.                     } catch (err) { 
  11.                         reject(err); 
  12.                     } 
  13.                 }, delay); 
  14.             }); 
  15.         }, 
  16.         cancel: () => { 
  17.             clearTimeout(ticket); 
  18.         } 
  19.     }; 
  20. }; 

基础版本

实现代码

注意点:

1.instance !== undefined这个作为判断是否实例化,也就是说可以是null, 仅仅一次的场景下使用,最适合不过了。

这里也是一个局限,如果就是返回undefined呢, 我保持沉默。

2.有人可能会吐槽我,你之前还说过 undefined不可靠,我微微一笑,你觉得迷人吗?

失败之后 initializing = false这个意图,就是某次初始化失败时,会通知之前的全部请求,已失败。

之后的请求,还会尝试初始化。

  1. import { delay } from "../util"
  2.  
  3. function asyncFactory() { 
  4.     let requests = []; 
  5.     let instance; 
  6.     let initializing = false
  7.  
  8.     return function initiator(fn: (...args: any) => Promise<any>) { 
  9.          // 实例已经实例化过了 
  10.          if (instance !== undefined){ 
  11.             return Promise.resolve(instance); 
  12.         } 
  13.         // 初始化中 
  14.         if (initializing) { 
  15.             return new Promise((resolve, reject) => { 
  16.                 // 保存请求 
  17.                 requests.push({ 
  18.                     resolve, 
  19.                     reject 
  20.                 }); 
  21.             }) 
  22.         } 
  23.         initializing = true
  24.         return new Promise((resolve, reject) => { 
  25.             // 保存请求 
  26.             requests.push({ 
  27.                 resolve, 
  28.                 reject 
  29.             }); 
  30.  
  31.             fn() 
  32.                 .then(result => { 
  33.                     instance = result; 
  34.                     initializing = false
  35.                     processRequests('resolve', instance); 
  36.                 }) 
  37.                 .catch(error => { 
  38.                     initializing = false
  39.                     processRequests('reject', error); 
  40.                 }); 
  41.         }); 
  42.     } 
  43.     function processRequests(type: "resolve" | "reject", value: any) { 
  44.         // 挨个resolve 
  45.         requests.forEach(q => { 
  46.             q[type](value "type"); 
  47.         }); 
  48.         // 置空请求,之后直接用instance 
  49.         requests = []; 
  50.     } 

测试代码

  1. const factory = asyncFactory(); 
  2.  
  3. async function asyncInsCreator() { 
  4.     await delay(2000).run(); 
  5.     return new Object(); 
  6.  
  7. function getAsyncIns() { 
  8.     return factory(asyncInsCreator); 
  9.  
  10. ; (async function test() { 
  11.     try {   
  12.  
  13.         const [ins1, ins2, ins3] = await Promise.all([ 
  14.             getAsyncIns(), 
  15.             getAsyncIns(), 
  16.             getAsyncIns() 
  17.         ]); 
  18.  
  19.         console.log("ins1:", ins1);  // ins1: {} 
  20.         console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true 
  21.         console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true 
  22.         console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true 
  23.     } catch (err) { 
  24.         console.log("err", err); 
  25.     } 
  26.  
  27. })(); 

存在的问题:

没法传参啊,没法设置this的上下文啊。

传递参数版本

实现思路:

  1. 增加参数 context 以及 args参数
  2. Function.prototype.appy

实现代码

  1. import { delay } from "../util"
  2.  
  3. interface AVFunction<T = unknown> { 
  4.     (value: T): void 
  5.  
  6. function asyncFactory<R = unknown, RR = unknown>() { 
  7.     let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = []; 
  8.     let instance: R; 
  9.     let initializing = false
  10.  
  11.     return function initiator(fn: (...args: any) => Promise<R>,  
  12.     context: unknown, ...args: unknown[]): Promise<R> { 
  13.         // 实例已经实例化过了 
  14.         if (instance !== undefined){ 
  15.             return Promise.resolve(instance); 
  16.         } 
  17.         // 初始化中 
  18.         if (initializing) { 
  19.             return new Promise((resolve, reject) => { 
  20.                 requests.push({ 
  21.                     resolve, 
  22.                     reject 
  23.                 }) 
  24.             }) 
  25.         } 
  26.         initializing = true 
  27.         return new Promise((resolve, reject) => { 
  28.             requests.push({ 
  29.                 resolve, 
  30.                 reject 
  31.             }) 
  32.  
  33.             fn.apply(context, args) 
  34.                 .then(res => { 
  35.                     instance = res; 
  36.                     initializing = false
  37.                     processRequests('resolve', instance); 
  38.                 }) 
  39.                 .catch(error => { 
  40.                     initializing = false
  41.                     processRequests('reject', error); 
  42.                 }) 
  43.         }) 
  44.     } 
  45.  
  46.     function processRequests(type: "resolve" | "reject", value: any) { 
  47.         // 挨个resolve 
  48.         requests.forEach(q => { 
  49.             q[type](value "type"); 
  50.         }); 
  51.         // 置空请求,之后直接用instance 
  52.         requests = []; 
  53.     } 

测试代码

  1. interface RES { 
  2.     p1: number 
  3.  
  4. const factory = asyncFactory<RES>(); 
  5.  
  6. async function asyncInsCreator(opitons: unknown = {}) { 
  7.     await delay(2000).run(); 
  8.     console.log("context.name", this.name); 
  9.     const result = new Object(opitons) as RES; 
  10.     return result; 
  11.  
  12. function getAsyncIns(context: unknown, options: unknown = {}) { 
  13.     return factory(asyncInsCreator, context, options); 
  14.  
  15. ; (async function test() { 
  16.  
  17.     try { 
  18.         const context = { 
  19.             name"context" 
  20.         }; 
  21.  
  22.         const [ins1, ins2, ins3] = await Promise.all([ 
  23.             getAsyncIns(context, { p1: 1 }), 
  24.             getAsyncIns(context, { p1: 2 }), 
  25.             getAsyncIns(context, { p1: 3 }) 
  26.         ]); 
  27.  
  28.         console.log("ins1:", ins1, ins1.p1); 
  29.         console.log("ins1=== ins2", ins1 === ins2); 
  30.         console.log("ins2=== ins3", ins2 === ins3); 
  31.         console.log("ins3=== ins1", ins3 === ins1); 
  32.     } catch (err) { 
  33.         console.log("err", err); 
  34.     } 
  35.  
  36. })(); 

存在的问题

看似完美,要是超时了,怎么办呢?

想到这个问题的人,品论区发文,我给你们点赞。

超时版本

这里就需要借用我们的工具方法delay:

  • 如果超时没有成功,通知所有请求失败。
  • 反之,通知所有请求成功。

实现代码

  1. import { delay } from "../util"
  2.  
  3. interface AVFunction<T = unknown> { 
  4.     (value: T): void 
  5.  
  6. function asyncFactory<R = unknown, RR = unknown>(timeout: number = 5 * 1000) { 
  7.     let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = []; 
  8.     let instance: R; 
  9.     let initializing = false
  10.  
  11.     return function initiator(fn: (...args: any) => Promise<R>, context: unknown, ...args: unknown[]): Promise<R> { 
  12.  
  13.         // 实例已经实例化过了 
  14.         if (instance !== undefined){ 
  15.             return Promise.resolve(instance); 
  16.         } 
  17.  
  18.         // 初始化中 
  19.         if (initializing) { 
  20.             return new Promise((resolve, reject) => { 
  21.                 requests.push({ 
  22.                     resolve, 
  23.                     reject 
  24.                 }) 
  25.             }) 
  26.         } 
  27.  
  28.         initializing = true 
  29.         return new Promise((resolve, reject) => { 
  30.  
  31.             requests.push({ 
  32.                 resolve, 
  33.                 reject 
  34.             }) 
  35.  
  36.             const { run, cancel } = delay(timeout); 
  37.  
  38.             run().then(() => { 
  39.                 const error = new Error("操作超时"); 
  40.                 processRequests("reject", error); 
  41.             }); 
  42.  
  43.             fn.apply(context, args) 
  44.                 .then(res => { 
  45.                     // 初始化成功 
  46.                     cancel(); 
  47.                     instance = res; 
  48.                     initializing = false
  49.                     processRequests('resolve', instance); 
  50.                 }) 
  51.                 .catch(error => { 
  52.                     // 初始化失败 
  53.                     cancel(); 
  54.                     initializing = false
  55.                     processRequests('reject', error); 
  56.                 }) 
  57.         }) 
  58.     } 
  59.  
  60.     function processRequests(type: "resolve" | "reject", value: any) { 
  61.         // 挨个resolve 
  62.         requests.forEach(q => { 
  63.             q[type](value "type"); 
  64.         }); 
  65.         // 置空请求,之后直接用instance 
  66.         requests = []; 
  67.     } 
  68.  
  69. interface RES { 
  70.     p1: number 
  71. const factory = asyncFactory<RES>(); 
  72.  
  73. async function asyncInsCreator(opitons: unknown = {}) { 
  74.     await delay(1000).run(); 
  75.     console.log("context.name", this.name); 
  76.     const result = new Object(opitons) as RES; 
  77.     return result; 
  78.  
  79. function getAsyncIns(context: unknown, options: unknown = {}) { 
  80.     return factory(asyncInsCreator, context, options); 
  81. ; (async function test() { 
  82.  
  83.     try { 
  84.         const context = { 
  85.             name"context" 
  86.         }; 
  87.  
  88.         const [instance1, instance2, instance3] = await Promise.all([ 
  89.             getAsyncIns(context, { p1: 1 }), 
  90.             getAsyncIns(context, { p1: 2 }), 
  91.             getAsyncIns(context, { p1: 3 }) 
  92.         ]); 
  93.  
  94.         console.log("instance1:", instance1, instance1.p1); 
  95.         console.log("instance1=== instance2", instance1 === instance2); 
  96.         console.log("instance2=== instance3", instance2 === instance3); 
  97.         console.log("instance3=== instance1", instance3 === instance1); 
  98.     } catch (err) { 
  99.         console.log("err", err); 
  100.     } 
  101. })(); 

测试代码

当把asyncInsCreator的 delay(1000)修改为 delay(6000)的时候,创建所以的事件6000ms大于 asyncFactory默认的5000ms,就会抛出下面的异常。

  1. err Error: 操作超时 
  2.     at c:\projects-github\juejinBlogs\异步单例\queue\args_timeout.ts:40:31 
  1. interface RES { 
  2.     p1: number 
  3.  
  4. const factory = asyncFactory<RES>(); 
  5.  
  6.  
  7. async function asyncInsCreator(opitons: unknown = {}) { 
  8.     await delay(1000).run(); 
  9.     console.log("context.name", this.name); 
  10.     const result = new Object(opitons) as RES; 
  11.     return result; 
  12.  
  13. function getAsyncIns(context: unknown, options: unknown = {}) { 
  14.     return factory(asyncInsCreator, context, options); 
  15.  
  16. ; (async function test() { 
  17.     try { 
  18.         const context = { 
  19.             name"context" 
  20.         }; 
  21.         const [ins1, ins2, ins3] = await Promise.all([ 
  22.             getAsyncIns(context, { p1: 1 }), 
  23.             getAsyncIns(context, { p1: 2 }), 
  24.             getAsyncIns(context, { p1: 3 }) 
  25.         ]); 
  26.  
  27.         console.log("ins1:", ins1, ins1.p1); 
  28.         console.log("ins1=== ins2", ins1 === ins2); 
  29.         console.log("ins2=== ins3", ins2 === ins3); 
  30.         console.log("ins3=== ins1", ins3 === ins1); 
  31.     } catch (err) { 
  32.         console.log("err", err); 
  33.     } 
  34. })(); 

存在的问题

存在的问题:

  1. 抛出了的Error new Error("操作超时")我们简单粗暴的抛出了这个异常,当外围的try/catch捕获后,还没法区别这个错误的来源。我们可以再封住一个AsyncFactoryError,或者 asyncInsCreator 抛出特定一定,交给try/catch 自身去识别。
  2. 没有判断参数 fn如果不是一个有效的函数,fn执行后是不是一个返回Promise。

是不是一个有效的函数好判断。

执行后是不是返回一个Promise, 借巨人p-is-promise[1]肩膀一靠。

  1. // 核心代码 
  2. function isPromise(value) { 
  3.    return value instanceof Promise || 
  4.     ( 
  5.      isObject(value) && 
  6.      typeof value.then === 'function' && 
  7.      typeof value.catch === 'function' 
  8.     ); 

存在问题,你就不解决了吗?不解决,等你来动手。

基于订阅发布模式的版本

这里是实现的另外一种思路, 利用订阅发布者。

要点

通过在Promise监听EventEmitter事件, 这里因为只需要监听一次,once闪亮登场。

  1. new Promise((resolve, reject) => { 
  2.     emitter.once("initialized", () => { 
  3.         resolve(instance); 
  4.     }); 
  5.     emitter.once("error", (error) => { 
  6.         reject(error); 
  7.     }); 
  8. }); 

实现代码

这里就实现一个最基础版本,至于带上下文,参数,超时的版本,大家可以尝试自己实现。

  1. import { EventEmitter } from "events"
  2. import { delay } from "./util"
  3.  
  4. function asyncFactory<R = any>() { 
  5.     let emitter = new EventEmitter(); 
  6.     let instance: any = null
  7.     let initializing = false
  8.  
  9.     return function getAsyncInstance(factory: () => Promise<R>): Promise<R> { 
  10.         // 已初始化完毕 
  11.         if (instance !== undefined){ 
  12.             return Promise.resolve(instance); 
  13.         } 
  14.         // 初始化中 
  15.         if (initializing === true) { 
  16.             return new Promise((resolve, reject) => { 
  17.                 emitter.once("initialized", () => { 
  18.                     resolve(instance); 
  19.                 }); 
  20.                 emitter.once("error", (error) => { 
  21.                     reject(error); 
  22.                 }); 
  23.             }); 
  24.         } 
  25.  
  26.         initializing = true
  27.         return new Promise((resolve, reject) => { 
  28.             emitter.once("initialized", () => { 
  29.                 resolve(instance); 
  30.             }); 
  31.             emitter.once("error", (error) => { 
  32.                 reject(error); 
  33.             }); 
  34.             factory() 
  35.                 .then(ins => { 
  36.                     instance = ins; 
  37.                     initializing = false
  38.                     emitter.emit("initialized"); 
  39.                     emitter = null
  40.                 }) 
  41.                 .catch((error) => { 
  42.                     initializing = false
  43.                     emitter.emit("error", error); 
  44.                 }); 
  45.         }) 
  46.     } 

总结

异步单例不多见,这里要表达的是一种思想,把基于事件的编程,变为基于Promise的编程。

这里其实还涉及一些设计模式, 学以致用,投入实际代码中,解决问题,带来收益,这才是我们追求的。

async-init[2]

Is it impossible to create a reliable async singleton pattern in JavaScript?[3]

Creating an async singletone in javascript[4]

参考资料

[1]p-is-promise: https://www.npmjs.com/package/p-is-promise

[2]async-init: https://github.com/ert78gb/async-init

[3]Is it impossible to create a reliable async singleton pattern in JavaScript?: https://stackoverflow.com/questions/58919867/is-it-impossible-to-create-a-reliable-async-singleton-pattern-in-javascript

[4]Creating an async singletone in javascript: https://stackoverflow.com/questions/59612076/creating-an-async-singletone-in-javascript

 

责任编辑:武晓燕 来源: 云的程序世界
相关推荐

2016-03-28 10:23:11

Android设计单例

2023-06-05 07:55:31

2022-06-07 08:55:04

Golang单例模式语言

2021-03-02 08:50:31

设计单例模式

2021-02-01 10:01:58

设计模式 Java单例模式

2022-09-29 08:39:37

架构

2015-09-06 11:07:52

C++设计模式单例模式

2013-11-26 16:20:26

Android设计模式

2024-02-04 12:04:17

2022-02-06 22:30:36

前端设计模式

2021-02-07 23:58:10

单例模式对象

2011-03-16 10:13:31

java单例模式

2024-03-06 13:19:19

工厂模式Python函数

2016-10-09 09:37:49

javascript单例模式

2012-12-20 10:17:32

IT运维

2012-03-07 17:24:10

戴尔咨询

2023-11-21 21:39:38

单例模式音频管理器

2011-06-28 15:18:45

Qt 单例模式

2019-06-11 09:50:07

SparkBroadcast代码

2021-08-11 17:22:11

设计模式单例
点赞
收藏

51CTO技术栈公众号