前言
异步单例:
创建实例需要一定的时间,创建期间,交出执行权,创建完毕后,拿回执行权,返回结果。
有人可能会吐槽,就这,其他方案分分钟搞定。没错,没有谁不可被替代。
这里主要表达的是一种编程思想,其能改变代码风格, 特定情况下漂亮的解决问题。多一种手段,多一种选择。
先一起来看一个栗子:
asyncInsCreator延时2秒创建一个对象;
getAsyncIns 封装异步对象获取过程;
我们多次调用 getAsyncIns, 得到同一个对象。
- asyncfunctionasyncInsCreator(){
- awaitdelay(2000).run();
- returnnewObject();
- }
- functiongetAsyncIns(){
- returnfactory(asyncInsCreator);
- }
- ;(asyncfunctiontest(){
- try{
- const[ins1,ins2,ins3]=awaitPromise.all([
- getAsyncIns(),
- getAsyncIns(),
- getAsyncIns()
- ]);
- console.log("ins1:",ins1);//ins1:{}
- console.log("ins1===ins2",ins1===ins2);//ins1===ins2true
- console.log("ins2===ins3",ins2===ins3);//ins2===ins3true
- console.log("ins3===ins1",ins3===ins1);//ins3===ins1true
- }catch(err){
- console.log("err",err);
- }
- })();
适用场景
异步单例
比如初始化socket.io客户端, indexedDB等等
仅仅一次的情况
举一个例子,我们可以注册多个 load事件
- window.addEventListener("load",function(){
- //othercode
- console.log("load1");
- });
- window.addEventListener("load",function(){
- //othercode
- console.log("load2");
- );
这要是换做React或者Vue,你先得订阅还得取消订阅,显得麻烦,当然你可以利用订阅发布思想再包装一层:
如果换成如下,是不是赏心悦目:
- awaitloaded();
- //TODO::
你肯定说,这个我会:
- functionloaded(){
- returnnewPromise((resove,reject)=>{
- window.addEventListener("load",resove)
- });
- }
我给你一段测试代码:
下面只会输出 loaded 1,不会输出loaded 2。
至于原因:load事件只会触发一次。
- functionloaded(){
- returnnewPromise((resolve,reject)=>{
- window.addEventListener("load",()=>resolve(null));
- });
- }
- asyncfunctiontest(){
- awaitloaded();
- console.log("loaded1");
- setTimeout(async()=>{
- awaitloaded();
- console.log("loaded2");
- },1000)
- }
- est();
到这里,我们的异步单例就可以秀一把,虽然他本意不是干这个,但他可以,因为他满足仅仅一次的条件。
loaded 1 与 loaded 2 都如期到来。
- constfactory=asyncFactory();
- functionasyncInsCreator(){
- returnnewPromise((resove,reject)=>{
- window.addEventListener("load",)
- });
- }
- functionloaded(){
- returnfactory(asyncInsCreator)
- }
- asyncfunctiontest(){
- awaitloaded();
- console.log("loaded1");//loaded1
- setTimeout(async()=>{
- awaitloaded();
- console.log("loaded2");//loaded2
- },1000)
- }
- test();
实现思路
状态
实例创建,其实也就只有简简单单的两种状态:
- 创建中
- 创建完毕
难点在于,创建中的时候,又有新的请求来获取实例。
那么我们就需要一个队列或者数组来维护这些请求队列,等待实例创建完毕,再通知请求方。
如果实例化已经完毕,那么之后就直接返回实例就好了。
变量
我们这里就需要三个变量:
- instance 存储已经创建完毕的实例
- initializing 是否创建中
- requests 来保存哪些处于创建中,发过来的请求
工具方法
delay:
延时一定时间调用指定的函数。
用于后面的超时,和模拟延时。
- exportfunctiondelay(delay:number=5000,fn=()=>{},context=null){
- letticket=null;
- return{
- run(…args:any[]){
- returnnewPromise((resolve,reject)=>{
- ticket=setTimeout(async()=>{
- try{
- constres=awaitfn.apply(context,args);
- resolve(res);
- }catch(err){
- reject(err);
- }
- },delay);
- });
- },
- cancel:()=>{
- clearTimeout(ticket);
- }
- };
- };
基础版本
实现代码
注意点:
1.instance !== undefined这个作为判断是否实例化,也就是说可以是null, 仅仅一次的场景下使用,最适合不过了。
这里也是一个局限,如果就是返回undefined呢, 我保持沉默。
2.有人可能会吐槽我,你之前还说过 undefined不可靠,我微微一笑,你觉得迷人吗?
失败之后 initializing = false这个意图,就是某次初始化失败时,会通知之前的全部请求,已失败。
之后的请求,还会尝试初始化。
- import{delay}from"../util";
- functionasyncFactory(){
- letrequests=[];
- letinstance;
- letinitializing=false;
- returnfunctioninitiator(fn:(…args:any)=>Promise<any>){
- //实例已经实例化过了
- if(instance!==undefined){
- returnPromise.resolve(instance);
- }
- //初始化中
- if(initializing){
- returnnewPromise((resolve,reject)=>{
- //保存请求
- requests.push({
- resolve,
- reject
- });
- })
- }
- initializing=true;
- returnnewPromise((resolve,reject)=>{
- //保存请求
- requests.push({
- resolve,
- reject
- });
- fn()
- .then(result=>{
- instance=result;
- initializing=false;
- processRequests('resolve',instance);
- })
- .catch(error=>{
- initializing=false;
- processRequests('reject',error);
- });
- });
- }
- functionprocessRequests(type:"resolve"|"reject",value:any){
- //挨个resolve
- requests.forEach(q=>{
- q[type](value"type");
- });
- //置空请求,之后直接用instance
- requests=[];
- }
- }
测试代码
- constfactory=asyncFactory();
- asyncfunctionasyncInsCreator(){
- awaitdelay(2000).run();
- returnnewObject();
- }
- functiongetAsyncIns(){
- returnfactory(asyncInsCreator);
- }
- ;(asyncfunctiontest(){
- try{
- const[ins1,ins2,ins3]=awaitPromise.all([
- getAsyncIns(),
- getAsyncIns(),
- getAsyncIns()
- ]);
- console.log("ins1:",ins1);//ins1:{}
- console.log("ins1===ins2",ins1===ins2);//ins1===ins2true
- console.log("ins2===ins3",ins2===ins3);//ins2===ins3true
- console.log("ins3===ins1",ins3===ins1);//ins3===ins1true
- }catch(err){
- console.log("err",err);
- }
- })();
存在的问题:
没法传参啊,没法设置this的上下文啊。
传递参数版本
实现思路:
- 增加参数 context 以及 args参数
- Function.prototype.appy
实现代码
- import{delay}from"../util";
- interfaceAVFunction<T=unknown>{
- (value:T):void
- }
- functionasyncFactory<R=unknown,RR=unknown>(){
- letrequests:{reject:AVFunction<RR>,resolve:AVFunction<R>}[]=[];
- letinstance:R;
- letinitializing=false;
- returnfunctioninitiator(fn:(…args:any)=>Promise<R>,
- context:unknown,…args:unknown[]):Promise<R>{
- //实例已经实例化过了
- if(instance!==undefined){
- returnPromise.resolve(instance);
- }
- //初始化中
- if(initializing){
- returnnewPromise((resolve,reject)=>{
- requests.push({
- resolve,
- reject
- })
- })
- }
- initializing=true
- returnnewPromise((resolve,reject)=>{
- requests.push({
- resolve,
- reject
- })
- fn.apply(context,args)
- .then(res=>{
- instance=res;
- initializing=false;
- processRequests('resolve',instance);
- })
- .catch(error=>{
- initializing=false;
- processRequests('reject',error);
- })
- })
- }
- functionprocessRequests(type:"resolve"|"reject",value:any){
- //挨个resolve
- requests.forEach(q=>{
- q[type](value"type");
- });
- //置空请求,之后直接用instance
- requests=[];
- }
- }
测试代码
- interfaceRES{
- p1:number
- }
- constfactory=asyncFactory<RES>();
- asyncfunctionasyncInsCreator(opitons:unknown={}){
- awaitdelay(2000).run();
- console.log("context.name",this.name);
- constresult=newObject(opitons)asRES;
- returnresult;
- }
- functiongetAsyncIns(context:unknown,options:unknown={}){
- returnfactory(asyncInsCreator,context,options);
- }
- ;(asyncfunctiontest(){
- try{
- constcontext={
- name:"context"
- };
- const[ins1,ins2,ins3]=awaitPromise.all([
- getAsyncIns(context,{p1:1}),
- getAsyncIns(context,{p1:2}),
- getAsyncIns(context,{p1:3})
- ]);
- console.log("ins1:",ins1,ins1.p1);
- console.log("ins1===ins2",ins1===ins2);
- console.log("ins2===ins3",ins2===ins3);
- console.log("ins3===ins1",ins3===ins1);
- }catch(err){
- console.log("err",err);
- }
- })();
存在的问题
看似完美,要是超时了,怎么办呢?
想到这个问题的人,品论区发文,我给你们点赞。
超时版本
这里就需要借用我们的工具方法delay:
- 如果超时没有成功,通知所有请求失败。
- 反之,通知所有请求成功。
实现代码
- import{delay}from"../util";
- interfaceAVFunction<T=unknown>{
- (value:T):void
- }
- functionasyncFactory<R=unknown,RR=unknown>(timeout:number=5*1000){
- letrequests:{reject:AVFunction<RR>,resolve:AVFunction<R>}[]=[];
- letinstance:R;
- letinitializing=false;
- returnfunctioninitiator(fn:(…args:any)=>Promise<R>,context:unknown,…args:unknown[]):Promise<R>{
- //实例已经实例化过了
- if(instance!==undefined){
- returnPromise.resolve(instance);
- }
- //初始化中
- if(initializing){
- returnnewPromise((resolve,reject)=>{
- requests.push({
- resolve,
- reject
- })
- })
- }
- initializing=true
- returnnewPromise((resolve,reject)=>{
- requests.push({
- resolve,
- reject
- })
- const{run,cancel}=delay(timeout);
- run().then(()=>{
- consterror=newError("操作超时");
- processRequests("reject",error);
- });
- fn.apply(context,args)
- .then(res=>{
- //初始化成功
- cancel();
- instance=res;
- initializing=false;
- processRequests('resolve',instance);
- })
- .catch(error=>{
- //初始化失败
- cancel();
- initializing=false;
- processRequests('reject',error);
- })
- })
- }
- functionprocessRequests(type:"resolve"|"reject",value:any){
- //挨个resolve
- requests.forEach(q=>{
- q[type](value"type");
- });
- //置空请求,之后直接用instance
- requests=[];
- }
- }
- interfaceRES{
- p1:number
- }
- constfactory=asyncFactory<RES>();
- asyncfunctionasyncInsCreator(opitons:unknown={}){
- awaitdelay(1000).run();
- console.log("context.name",this.name);
- constresult=newObject(opitons)asRES;
- returnresult;
- }
- functiongetAsyncIns(context:unknown,options:unknown={}){
- returnfactory(asyncInsCreator,context,options);
- }
- ;(asyncfunctiontest(){
- try{
- constcontext={
- name:"context"
- };
- const[instance1,instance2,instance3]=awaitPromise.all([
- getAsyncIns(context,{p1:1}),
- getAsyncIns(context,{p1:2}),
- getAsyncIns(context,{p1:3})
- ]);
- console.log("instance1:",instance1,instance1.p1);
- console.log("instance1===instance2",instance1===instance2);
- console.log("instance2===instance3",instance2===instance3);
- console.log("instance3===instance1",instance3===instance1);
- }catch(err){
- console.log("err",err);
- }
- })();
测试代码
当把asyncInsCreator的 delay(1000)修改为 delay(6000)的时候,创建所以的事件6000ms大于 asyncFactory默认的5000ms,就会抛出下面的异常。
- errError:操作超时
- atc:\\projects-github\\juejinBlogs\\异步单例\\queue\\args_timeout.ts:40:31
- interfaceRES{
- p1:number
- }
- constfactory=asyncFactory<RES>();
- asyncfunctionasyncInsCreator(opitons:unknown={}){
- awaitdelay(1000).run();
- console.log("context.name",this.name);
- constresult=newObject(opitons)asRES;
- returnresult;
- }
- functiongetAsyncIns(context:unknown,options:unknown={}){
- returnfactory(asyncInsCreator,context,options);
- }
- ;(asyncfunctiontest(){
- try{
- constcontext={
- name:"context"
- };
- const[ins1,ins2,ins3]=awaitPromise.all([
- getAsyncIns(context,{p1:1}),
- getAsyncIns(context,{p1:2}),
- getAsyncIns(context,{p1:3})
- ]);
- console.log("ins1:",ins1,ins1.p1);
- console.log("ins1===ins2",ins1===ins2);
- console.log("ins2===ins3",ins2===ins3);
- console.log("ins3===ins1",ins3===ins1);
- }catch(err){
- console.log("err",err);
- }
- })();
存在的问题
存在的问题:
- 抛出了的Error new Error("操作超时")我们简单粗暴的抛出了这个异常,当外围的try/catch捕获后,还没法区别这个错误的来源。我们可以再封住一个AsyncFactoryError,或者 asyncInsCreator 抛出特定一定,交给try/catch 自身去识别。
- 没有判断参数 fn如果不是一个有效的函数,fn执行后是不是一个返回Promise。
是不是一个有效的函数好判断。
执行后是不是返回一个Promise, 借巨人p-is-promise[1]肩膀一靠。
- //核心代码
- functionisPromise(value){
- returnvalueinstanceofPromise||
- (
- isObject(value)&&
- typeofvalue.then==='function'&&
- typeofvalue.catch==='function'
- );
- }
存在问题,你就不解决了吗?不解决,等你来动手。
基于订阅发布模式的版本
这里是实现的另外一种思路, 利用订阅发布者。
要点
通过在Promise监听EventEmitter事件, 这里因为只需要监听一次,once闪亮登场。
- newPromise((resolve,reject)=>{
- emitter.once("initialized",()=>{
- resolve(instance);
- });
- emitter.once("error",(error)=>{
- reject(error);
- });
- });
实现代码
这里就实现一个最基础版本,至于带上下文,参数,超时的版本,大家可以尝试自己实现。
- import{EventEmitter}from"events";
- import{delay}from"./util";
- functionasyncFactory<R=any>(){
- letemitter=newEventEmitter();
- letinstance:any=null;
- letinitializing=false;
- returnfunctiongetAsyncInstance(factory:()=>Promise<R>):Promise<R>{
- //已初始化完毕
- if(instance!==undefined){
- returnPromise.resolve(instance);
- }
- //初始化中
- if(initializing===true){
- returnnewPromise((resolve,reject)=>{
- emitter.once("initialized",()=>{
- resolve(instance);
- });
- emitter.once("error",(error)=>{
- reject(error);
- });
- });
- }
- initializing=true;
- returnnewPromise((resolve,reject)=>{
- emitter.once("initialized",()=>{
- resolve(instance);
- });
- emitter.once("error",(error)=>{
- reject(error);
- });
- factory()
- .then(ins=>{
- instance=ins;
- initializing=false;
- emitter.emit("initialized");
- emitter=null;
- })
- .catch((error)=>{
- initializing=false;
- emitter.emit("error",error);
- });
- })
- }
- }
总结
异步单例不多见,这里要表达的是一种思想,把基于事件的编程,变为基于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