異步單例模式之不一樣的單例模式
本文轉(zhuǎn)載自微信公眾號「云的程序世界」,作者云的世界。轉(zhuǎn)載本文請聯(lián)系云的程序世界公眾號。
前言
單例模式大家都知道,異步單例又為何物。
異步單例:
創(chuàng)建實例需要一定的時間,創(chuàng)建期間,交出執(zhí)行權,創(chuàng)建完畢后,拿回執(zhí)行權,返回結(jié)果。
有人可能會吐槽,就這,其他方案分分鐘搞定。沒錯,沒有誰不可被替代。
這里主要表達的是一種編程思想,其能改變代碼風格, 特定情況下漂亮的解決問題。多一種手段,多一種選擇。
先一起來看一個栗子:
asyncInsCreator延時2秒創(chuàng)建一個對象;
getAsyncIns 封裝異步對象獲取過程;
我們多次調(diào)用 getAsyncIns, 得到同一個對象。
- async function asyncInsCreator() {
- await delay(2000).run();
- return new Object();
- }
- function getAsyncIns() {
- return factory(asyncInsCreator);
- }
- ; (async function test() {
- try {
- const [ins1, ins2, ins3] = await Promise.all([
- getAsyncIns(),
- getAsyncIns(),
- getAsyncIns()
- ]);
- console.log("ins1:", ins1); // ins1: {}
- console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true
- console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true
- console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true
- } catch (err) {
- console.log("err", err);
- }
- })();
適用場景
異步單例
比如初始化socket.io客戶端, indexedDB等等
僅僅一次的情況
舉一個例子,我們可以注冊多個 load事件
- window.addEventListener("load", function () {
- // other code
- console.log("load 1");
- });
- window.addEventListener("load", function () {
- // other code
- console.log("load 2");
- );
這要是換做React或者Vue,你先得訂閱還得取消訂閱,顯得麻煩,當然你可以利用訂閱發(fā)布思想再包裝一層:
如果換成如下,是不是賞心悅目:
- await loaded();
- // TODO::
你肯定說,這個我會:
- function loaded() {
- return new Promise((resove, reject) => {
- window.addEventListener("load", resove)
- });
- }
我給你一段測試代碼:
下面只會輸出 loaded 1,不會輸出loaded 2。
至于原因:load事件只會觸發(fā)一次。
- function loaded() {
- return new Promise((resolve, reject) => {
- window.addEventListener("load", ()=> resolve(null));
- });
- }
- async function test() {
- await loaded();
- console.log("loaded 1");
- setTimeout(async () => {
- await loaded();
- console.log("loaded 2");
- }, 1000)
- }
- est();
到這里,我們的異步單例就可以秀一把,雖然他本意不是干這個,但他可以,因為他滿足僅僅一次的條件。
我們看看使用異步單例模式的代碼:
loaded 1 與 loaded 2 都如期到來。
- const factory = asyncFactory();
- function asyncInsCreator() {
- return new Promise((resove, reject) => {
- window.addEventListener("load", )
- });
- }
- function loaded() {
- return factory(asyncInsCreator)
- }
- async function test() {
- await loaded();
- console.log("loaded 1"); // loaded 1
- setTimeout(async () => {
- await loaded();
- console.log("loaded 2"); // loaded 2
- }, 1000)
- }
- test();
實現(xiàn)思路
狀態(tài)
實例創(chuàng)建,其實也就只有簡簡單單的兩種狀態(tài):
- 創(chuàng)建中
- 創(chuàng)建完畢
難點在于,創(chuàng)建中的時候,又有新的請求來獲取實例。
那么我們就需要一個隊列或者數(shù)組來維護這些請求隊列,等待實例創(chuàng)建完畢,再通知請求方。
如果實例化已經(jīng)完畢,那么之后就直接返回實例就好了。
變量
我們這里就需要三個變量:
- instance 存儲已經(jīng)創(chuàng)建完畢的實例
- initializing 是否創(chuàng)建中
- requests 來保存哪些處于創(chuàng)建中,發(fā)過來的請求
工具方法
delay:
延時一定時間調(diào)用指定的函數(shù)。
用于后面的超時,和模擬延時。
- export function delay(delay: number = 5000, fn = () => { }, context = null) {
- let ticket = null;
- return {
- run(...args: any[]) {
- return new Promise((resolve, reject) => {
- ticket = setTimeout(async () => {
- try {
- const res = await fn.apply(context, args);
- resolve(res);
- } catch (err) {
- reject(err);
- }
- }, delay);
- });
- },
- cancel: () => {
- clearTimeout(ticket);
- }
- };
- };
基礎版本
實現(xiàn)代碼
注意點:
1.instance !== undefined這個作為判斷是否實例化,也就是說可以是null, 僅僅一次的場景下使用,最適合不過了。
這里也是一個局限,如果就是返回undefined呢, 我保持沉默。
2.有人可能會吐槽我,你之前還說過 undefined不可靠,我微微一笑,你覺得迷人嗎?
失敗之后 initializing = false這個意圖,就是某次初始化失敗時,會通知之前的全部請求,已失敗。
之后的請求,還會嘗試初始化。
- import { delay } from "../util";
- function asyncFactory() {
- let requests = [];
- let instance;
- let initializing = false;
- return function initiator(fn: (...args: any) => Promise<any>) {
- // 實例已經(jīng)實例化過了
- if (instance !== undefined){
- return Promise.resolve(instance);
- }
- // 初始化中
- if (initializing) {
- return new Promise((resolve, reject) => {
- // 保存請求
- requests.push({
- resolve,
- reject
- });
- })
- }
- initializing = true;
- return new Promise((resolve, reject) => {
- // 保存請求
- requests.push({
- resolve,
- reject
- });
- fn()
- .then(result => {
- instance = result;
- initializing = false;
- processRequests('resolve', instance);
- })
- .catch(error => {
- initializing = false;
- processRequests('reject', error);
- });
- });
- }
- function processRequests(type: "resolve" | "reject", value: any) {
- // 挨個resolve
- requests.forEach(q => {
- q[type](value "type");
- });
- // 置空請求,之后直接用instance
- requests = [];
- }
- }
測試代碼
- const factory = asyncFactory();
- async function asyncInsCreator() {
- await delay(2000).run();
- return new Object();
- }
- function getAsyncIns() {
- return factory(asyncInsCreator);
- }
- ; (async function test() {
- try {
- const [ins1, ins2, ins3] = await Promise.all([
- getAsyncIns(),
- getAsyncIns(),
- getAsyncIns()
- ]);
- console.log("ins1:", ins1); // ins1: {}
- console.log("ins1===ins2", ins1 === ins2); // ins1===ins2 true
- console.log("ins2===ins3", ins2 === ins3); // ins2===ins3 true
- console.log("ins3=== ins1", ins3 === ins1); // ins3=== ins1 true
- } catch (err) {
- console.log("err", err);
- }
- })();
存在的問題:
沒法傳參啊,沒法設置this的上下文啊。
傳遞參數(shù)版本
實現(xiàn)思路:
- 增加參數(shù) context 以及 args參數(shù)
- Function.prototype.appy
實現(xiàn)代碼
- import { delay } from "../util";
- interface AVFunction<T = unknown> {
- (value: T): void
- }
- function asyncFactory<R = unknown, RR = unknown>() {
- let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = [];
- let instance: R;
- let initializing = false;
- return function initiator(fn: (...args: any) => Promise<R>,
- context: unknown, ...args: unknown[]): Promise<R> {
- // 實例已經(jīng)實例化過了
- if (instance !== undefined){
- return Promise.resolve(instance);
- }
- // 初始化中
- if (initializing) {
- return new Promise((resolve, reject) => {
- requests.push({
- resolve,
- reject
- })
- })
- }
- initializing = true
- return new Promise((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);
- })
- })
- }
- function processRequests(type: "resolve" | "reject", value: any) {
- // 挨個resolve
- requests.forEach(q => {
- q[type](value "type");
- });
- // 置空請求,之后直接用instance
- requests = [];
- }
- }
測試代碼
- interface RES {
- p1: number
- }
- const factory = asyncFactory<RES>();
- async function asyncInsCreator(opitons: unknown = {}) {
- await delay(2000).run();
- console.log("context.name", this.name);
- const result = new Object(opitons) as RES;
- return result;
- }
- function getAsyncIns(context: unknown, options: unknown = {}) {
- return factory(asyncInsCreator, context, options);
- }
- ; (async function test() {
- try {
- const context = {
- name: "context"
- };
- const [ins1, ins2, ins3] = await Promise.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);
- }
- })();
存在的問題
看似完美,要是超時了,怎么辦呢?
想到這個問題的人,品論區(qū)發(fā)文,我給你們點贊。
超時版本
這里就需要借用我們的工具方法delay:
- 如果超時沒有成功,通知所有請求失敗。
- 反之,通知所有請求成功。
實現(xiàn)代碼
- import { delay } from "../util";
- interface AVFunction<T = unknown> {
- (value: T): void
- }
- function asyncFactory<R = unknown, RR = unknown>(timeout: number = 5 * 1000) {
- let requests: { reject: AVFunction<RR>, resolve: AVFunction<R> }[] = [];
- let instance: R;
- let initializing = false;
- return function initiator(fn: (...args: any) => Promise<R>, context: unknown, ...args: unknown[]): Promise<R> {
- // 實例已經(jīng)實例化過了
- if (instance !== undefined){
- return Promise.resolve(instance);
- }
- // 初始化中
- if (initializing) {
- return new Promise((resolve, reject) => {
- requests.push({
- resolve,
- reject
- })
- })
- }
- initializing = true
- return new Promise((resolve, reject) => {
- requests.push({
- resolve,
- reject
- })
- const { run, cancel } = delay(timeout);
- run().then(() => {
- const error = new Error("操作超時");
- 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);
- })
- })
- }
- function processRequests(type: "resolve" | "reject", value: any) {
- // 挨個resolve
- requests.forEach(q => {
- q[type](value "type");
- });
- // 置空請求,之后直接用instance
- requests = [];
- }
- }
- interface RES {
- p1: number
- }
- const factory = asyncFactory<RES>();
- async function asyncInsCreator(opitons: unknown = {}) {
- await delay(1000).run();
- console.log("context.name", this.name);
- const result = new Object(opitons) as RES;
- return result;
- }
- function getAsyncIns(context: unknown, options: unknown = {}) {
- return factory(asyncInsCreator, context, options);
- }
- ; (async function test() {
- try {
- const context = {
- name: "context"
- };
- const [instance1, instance2, instance3] = await Promise.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)的時候,創(chuàng)建所以的事件6000ms大于 asyncFactory默認的5000ms,就會拋出下面的異常。
- err Error: 操作超時
- at c:\projects-github\juejinBlogs\異步單例\queue\args_timeout.ts:40:31
- interface RES {
- p1: number
- }
- const factory = asyncFactory<RES>();
- async function asyncInsCreator(opitons: unknown = {}) {
- await delay(1000).run();
- console.log("context.name", this.name);
- const result = new Object(opitons) as RES;
- return result;
- }
- function getAsyncIns(context: unknown, options: unknown = {}) {
- return factory(asyncInsCreator, context, options);
- }
- ; (async function test() {
- try {
- const context = {
- name: "context"
- };
- const [ins1, ins2, ins3] = await Promise.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捕獲后,還沒法區(qū)別這個錯誤的來源。我們可以再封住一個AsyncFactoryError,或者 asyncInsCreator 拋出特定一定,交給try/catch 自身去識別。
- 沒有判斷參數(shù) fn如果不是一個有效的函數(shù),fn執(zhí)行后是不是一個返回Promise。
是不是一個有效的函數(shù)好判斷。
執(zhí)行后是不是返回一個Promise, 借巨人p-is-promise[1]肩膀一靠。
- // 核心代碼
- function isPromise(value) {
- return value instanceof Promise ||
- (
- isObject(value) &&
- typeof value.then === 'function' &&
- typeof value.catch === 'function'
- );
- }
存在問題,你就不解決了嗎?不解決,等你來動手。
基于訂閱發(fā)布模式的版本
這里是實現(xiàn)的另外一種思路, 利用訂閱發(fā)布者。
要點
通過在Promise監(jiān)聽EventEmitter事件, 這里因為只需要監(jiān)聽一次,once閃亮登場。
- new Promise((resolve, reject) => {
- emitter.once("initialized", () => {
- resolve(instance);
- });
- emitter.once("error", (error) => {
- reject(error);
- });
- });
實現(xiàn)代碼
這里就實現(xiàn)一個最基礎版本,至于帶上下文,參數(shù),超時的版本,大家可以嘗試自己實現(xiàn)。
- import { EventEmitter } from "events";
- import { delay } from "./util";
- function asyncFactory<R = any>() {
- let emitter = new EventEmitter();
- let instance: any = null;
- let initializing = false;
- return function getAsyncInstance(factory: () => Promise<R>): Promise<R> {
- // 已初始化完畢
- if (instance !== undefined){
- return Promise.resolve(instance);
- }
- // 初始化中
- if (initializing === true) {
- return new Promise((resolve, reject) => {
- emitter.once("initialized", () => {
- resolve(instance);
- });
- emitter.once("error", (error) => {
- reject(error);
- });
- });
- }
- initializing = true;
- return new Promise((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);
- });
- })
- }
- }
總結(jié)
異步單例不多見,這里要表達的是一種思想,把基于事件的編程,變?yōu)榛赑romise的編程。
這里其實還涉及一些設計模式, 學以致用,投入實際代碼中,解決問題,帶來收益,這才是我們追求的。
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