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

2025-05-29 0 34

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

前言

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

异步单例:

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

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

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

先一起来看一个栗子:

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

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

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

  1. asyncfunctionasyncInsCreator(){
  2. awaitdelay(2000).run();
  3. returnnewObject();
  4. }
  5. functiongetAsyncIns(){
  6. returnfactory(asyncInsCreator);
  7. }
  8. ;(asyncfunctiontest(){
  9. try{
  10. const[ins1,ins2,ins3]=awaitPromise.all([
  11. getAsyncIns(),
  12. getAsyncIns(),
  13. getAsyncIns()
  14. ]);
  15. console.log("ins1:",ins1);//ins1:{}
  16. console.log("ins1===ins2",ins1===ins2);//ins1===ins2true
  17. console.log("ins2===ins3",ins2===ins3);//ins2===ins3true
  18. console.log("ins3===ins1",ins3===ins1);//ins3===ins1true
  19. }catch(err){
  20. console.log("err",err);
  21. }
  22. })();

适用场景

异步单例

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

仅仅一次的情况

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

  1. window.addEventListener("load",function(){
  2. //othercode
  3. console.log("load1");
  4. });
  5. window.addEventListener("load",function(){
  6. //othercode
  7. console.log("load2");
  8. );

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

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

  1. awaitloaded();
  2. //TODO::

你肯定说,这个我会:

  1. functionloaded(){
  2. returnnewPromise((resove,reject)=>{
  3. window.addEventListener("load",resove)
  4. });
  5. }

我给你一段测试代码:

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

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

  1. functionloaded(){
  2. returnnewPromise((resolve,reject)=>{
  3. window.addEventListener("load",()=>resolve(null));
  4. });
  5. }
  6. asyncfunctiontest(){
  7. awaitloaded();
  8. console.log("loaded1");
  9. setTimeout(async()=>{
  10. awaitloaded();
  11. console.log("loaded2");
  12. },1000)
  13. }
  14. est();

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

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

loaded 1 与 loaded 2 都如期到来。

  1. constfactory=asyncFactory();
  2. functionasyncInsCreator(){
  3. returnnewPromise((resove,reject)=>{
  4. window.addEventListener("load",)
  5. });
  6. }
  7. functionloaded(){
  8. returnfactory(asyncInsCreator)
  9. }
  10. asyncfunctiontest(){
  11. awaitloaded();
  12. console.log("loaded1");//loaded1
  13. setTimeout(async()=>{
  14. awaitloaded();
  15. console.log("loaded2");//loaded2
  16. },1000)
  17. }
  18. test();

实现思路

状态

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

  1. 创建中
  2. 创建完毕

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

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

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

变量

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

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

工具方法

delay:

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

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

  1. exportfunctiondelay(delay:number=5000,fn=()=>{},context=null){
  2. letticket=null;
  3. return{
  4. run(…args:any[]){
  5. returnnewPromise((resolve,reject)=>{
  6. ticket=setTimeout(async()=>{
  7. try{
  8. constres=awaitfn.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. functionasyncFactory(){
  3. letrequests=[];
  4. letinstance;
  5. letinitializing=false;
  6. returnfunctioninitiator(fn:(…args:any)=>Promise<any>){
  7. //实例已经实例化过了
  8. if(instance!==undefined){
  9. returnPromise.resolve(instance);
  10. }
  11. //初始化中
  12. if(initializing){
  13. returnnewPromise((resolve,reject)=>{
  14. //保存请求
  15. requests.push({
  16. resolve,
  17. reject
  18. });
  19. })
  20. }
  21. initializing=true;
  22. returnnewPromise((resolve,reject)=>{
  23. //保存请求
  24. requests.push({
  25. resolve,
  26. reject
  27. });
  28. fn()
  29. .then(result=>{
  30. instance=result;
  31. initializing=false;
  32. processRequests('resolve',instance);
  33. })
  34. .catch(error=>{
  35. initializing=false;
  36. processRequests('reject',error);
  37. });
  38. });
  39. }
  40. functionprocessRequests(type:"resolve"|"reject",value:any){
  41. //挨个resolve
  42. requests.forEach(q=>{
  43. q[type](value"type");
  44. });
  45. //置空请求,之后直接用instance
  46. requests=[];
  47. }
  48. }

测试代码

  1. constfactory=asyncFactory();
  2. asyncfunctionasyncInsCreator(){
  3. awaitdelay(2000).run();
  4. returnnewObject();
  5. }
  6. functiongetAsyncIns(){
  7. returnfactory(asyncInsCreator);
  8. }
  9. ;(asyncfunctiontest(){
  10. try{
  11. const[ins1,ins2,ins3]=awaitPromise.all([
  12. getAsyncIns(),
  13. getAsyncIns(),
  14. getAsyncIns()
  15. ]);
  16. console.log("ins1:",ins1);//ins1:{}
  17. console.log("ins1===ins2",ins1===ins2);//ins1===ins2true
  18. console.log("ins2===ins3",ins2===ins3);//ins2===ins3true
  19. console.log("ins3===ins1",ins3===ins1);//ins3===ins1true
  20. }catch(err){
  21. console.log("err",err);
  22. }
  23. })();

存在的问题:

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

传递参数版本

实现思路:

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

实现代码

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

测试代码

  1. interfaceRES{
  2. p1:number
  3. }
  4. constfactory=asyncFactory<RES>();
  5. asyncfunctionasyncInsCreator(opitons:unknown={}){
  6. awaitdelay(2000).run();
  7. console.log("context.name",this.name);
  8. constresult=newObject(opitons)asRES;
  9. returnresult;
  10. }
  11. functiongetAsyncIns(context:unknown,options:unknown={}){
  12. returnfactory(asyncInsCreator,context,options);
  13. }
  14. ;(asyncfunctiontest(){
  15. try{
  16. constcontext={
  17. name:"context"
  18. };
  19. const[ins1,ins2,ins3]=awaitPromise.all([
  20. getAsyncIns(context,{p1:1}),
  21. getAsyncIns(context,{p1:2}),
  22. getAsyncIns(context,{p1:3})
  23. ]);
  24. console.log("ins1:",ins1,ins1.p1);
  25. console.log("ins1===ins2",ins1===ins2);
  26. console.log("ins2===ins3",ins2===ins3);
  27. console.log("ins3===ins1",ins3===ins1);
  28. }catch(err){
  29. console.log("err",err);
  30. }
  31. })();

存在的问题

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

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

超时版本

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

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

实现代码

  1. import{delay}from"../util";
  2. interfaceAVFunction<T=unknown>{
  3. (value:T):void
  4. }
  5. functionasyncFactory<R=unknown,RR=unknown>(timeout:number=5*1000){
  6. letrequests:{reject:AVFunction<RR>,resolve:AVFunction<R>}[]=[];
  7. letinstance:R;
  8. letinitializing=false;
  9. returnfunctioninitiator(fn:(…args:any)=>Promise<R>,context:unknown,…args:unknown[]):Promise<R>{
  10. //实例已经实例化过了
  11. if(instance!==undefined){
  12. returnPromise.resolve(instance);
  13. }
  14. //初始化中
  15. if(initializing){
  16. returnnewPromise((resolve,reject)=>{
  17. requests.push({
  18. resolve,
  19. reject
  20. })
  21. })
  22. }
  23. initializing=true
  24. returnnewPromise((resolve,reject)=>{
  25. requests.push({
  26. resolve,
  27. reject
  28. })
  29. const{run,cancel}=delay(timeout);
  30. run().then(()=>{
  31. consterror=newError("操作超时");
  32. processRequests("reject",error);
  33. });
  34. fn.apply(context,args)
  35. .then(res=>{
  36. //初始化成功
  37. cancel();
  38. instance=res;
  39. initializing=false;
  40. processRequests('resolve',instance);
  41. })
  42. .catch(error=>{
  43. //初始化失败
  44. cancel();
  45. initializing=false;
  46. processRequests('reject',error);
  47. })
  48. })
  49. }
  50. functionprocessRequests(type:"resolve"|"reject",value:any){
  51. //挨个resolve
  52. requests.forEach(q=>{
  53. q[type](value"type");
  54. });
  55. //置空请求,之后直接用instance
  56. requests=[];
  57. }
  58. }
  59. interfaceRES{
  60. p1:number
  61. }
  62. constfactory=asyncFactory<RES>();
  63. asyncfunctionasyncInsCreator(opitons:unknown={}){
  64. awaitdelay(1000).run();
  65. console.log("context.name",this.name);
  66. constresult=newObject(opitons)asRES;
  67. returnresult;
  68. }
  69. functiongetAsyncIns(context:unknown,options:unknown={}){
  70. returnfactory(asyncInsCreator,context,options);
  71. }
  72. ;(asyncfunctiontest(){
  73. try{
  74. constcontext={
  75. name:"context"
  76. };
  77. const[instance1,instance2,instance3]=awaitPromise.all([
  78. getAsyncIns(context,{p1:1}),
  79. getAsyncIns(context,{p1:2}),
  80. getAsyncIns(context,{p1:3})
  81. ]);
  82. console.log("instance1:",instance1,instance1.p1);
  83. console.log("instance1===instance2",instance1===instance2);
  84. console.log("instance2===instance3",instance2===instance3);
  85. console.log("instance3===instance1",instance3===instance1);
  86. }catch(err){
  87. console.log("err",err);
  88. }
  89. })();

测试代码

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

  1. errError:操作超时
  2. atc:\\projects-github\\juejinBlogs\\异步单例\\queue\\args_timeout.ts:40:31
  1. interfaceRES{
  2. p1:number
  3. }
  4. constfactory=asyncFactory<RES>();
  5. asyncfunctionasyncInsCreator(opitons:unknown={}){
  6. awaitdelay(1000).run();
  7. console.log("context.name",this.name);
  8. constresult=newObject(opitons)asRES;
  9. returnresult;
  10. }
  11. functiongetAsyncIns(context:unknown,options:unknown={}){
  12. returnfactory(asyncInsCreator,context,options);
  13. }
  14. ;(asyncfunctiontest(){
  15. try{
  16. constcontext={
  17. name:"context"
  18. };
  19. const[ins1,ins2,ins3]=awaitPromise.all([
  20. getAsyncIns(context,{p1:1}),
  21. getAsyncIns(context,{p1:2}),
  22. getAsyncIns(context,{p1:3})
  23. ]);
  24. console.log("ins1:",ins1,ins1.p1);
  25. console.log("ins1===ins2",ins1===ins2);
  26. console.log("ins2===ins3",ins2===ins3);
  27. console.log("ins3===ins1",ins3===ins1);
  28. }catch(err){
  29. console.log("err",err);
  30. }
  31. })();

存在的问题

存在的问题:

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

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

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

  1. //核心代码
  2. functionisPromise(value){
  3. returnvalueinstanceofPromise||
  4. (
  5. isObject(value)&&
  6. typeofvalue.then==='function'&&
  7. typeofvalue.catch==='function'
  8. );
  9. }

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

基于订阅发布模式的版本

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

要点

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

  1. newPromise((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. functionasyncFactory<R=any>(){
  4. letemitter=newEventEmitter();
  5. letinstance:any=null;
  6. letinitializing=false;
  7. returnfunctiongetAsyncInstance(factory:()=>Promise<R>):Promise<R>{
  8. //已初始化完毕
  9. if(instance!==undefined){
  10. returnPromise.resolve(instance);
  11. }
  12. //初始化中
  13. if(initializing===true){
  14. returnnewPromise((resolve,reject)=>{
  15. emitter.once("initialized",()=>{
  16. resolve(instance);
  17. });
  18. emitter.once("error",(error)=>{
  19. reject(error);
  20. });
  21. });
  22. }
  23. initializing=true;
  24. returnnewPromise((resolve,reject)=>{
  25. emitter.once("initialized",()=>{
  26. resolve(instance);
  27. });
  28. emitter.once("error",(error)=>{
  29. reject(error);
  30. });
  31. factory()
  32. .then(ins=>{
  33. instance=ins;
  34. initializing=false;
  35. emitter.emit("initialized");
  36. emitter=null;
  37. })
  38. .catch((error)=>{
  39. initializing=false;
  40. emitter.emit("error",error);
  41. });
  42. })
  43. }
  44. }

总结

异步单例不多见,这里要表达的是一种思想,把基于事件的编程,变为基于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

原文链接:https://mp.weixin.qq.com/s/pccZbczQ4iEubuPdQDfQWw

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 异步单例模式之不一样的单例模式 https://www.kuaiidc.com/93095.html

相关文章

发表评论
暂无评论