分享个适用cocos单例的写法

你应该没理解我问的
单例还有一个需求是要首次指定调用的时候才初始化,以及大部分情况下可以自适应处理依赖关系
如果是我现在的做法就是

 // A.ts
 class A {}
 //B.ts
 class B{
     print(){
           Singleton.Instance(A).callA();
     }
 }

//F.ts //F是一个很大玩法, 需要初始大量的资源和数据
class F{}

//Test.ts //初始化B和A, 且B不用修改构造函数 也不会初始化F
Singleton.Instance(B).print()

//Test2.ts
update(){ //首次调用初始化, 并每次update获取进度,完成之后F可用, 并进入F的系统
    Singleton.Instance(F).XX
}

没差呀,你自己试试看就知道了。

// A.ts
class A {
    print() {
        console.log(`A.print`)
    }
}

//B.ts
class B {
    print() {
        fw.a.print();
    }
}
class F {
    /** 特殊初始化接口,只调用一次 */
    onceInit() {
        // 请求服务端数据,或者下载资源
    }

    update() {

    }
}
//Test2.ts
class TestCom extends Component {
    start() {
        fw.f.onceInit();
    }
    update() {
        fw.f.update();
        if(fw.f.done) {
            // 退出 update
        }
    }
}

至于你实例化的时候马上调用初始化,而且依赖 update 的,就应该单独一个函数,要不然很难理解你的这种潜规则的。
而且任务是否完成,理论上要放到内部去做驱动。外部只关心你成功没有,才不关心你要不要驱动。

我看懂你的意思了, 这个在某些情况下是有些问题的. 不适合我说的"开箱后才创建,自动管理依赖"的需求.
假设我有一个预加载的需求, 这个只能继续在GCtr里面继续拆分init列表, 初始化时机从自动(或说统一入口)变成手动, 若哪一天我改动了预加载类的调用关系, 这里有可能还需要手动调整.

是的,时机被移动到了某些业务逻辑中了。

我2.4的

type SingleInstance<T extends new (...args: any) => any> = T & {
    /**单利对象方法 */
    Ins: (...args: ConstructorParameters<T>) => InstanceType<T>;
   // __ins__?: InstanceType<T>;
};
/**
 * 单利方法  
 * target依然可以继承,不会有影响           
 * 推荐用法           
 * const MyClass = SingleFunc(class {})         
 * 或者            
 * const MyClass = SingleFunc(class extends ParentClass {})      
 * 或者               
 * const p:InstanceType<typeof(MyClass)>=new MyClass()          //这是特殊需求       
 * @param 
 * @returns 
 */
export function SingleFunc<T extends ConstructorTemplateType<any>>(target: T): SingleInstance<T> {
    const instanceKey = Symbol('singleton_instance')
    const ctor = target as SingleInstance<T>;
    // 添加静态方法 Ins  
    ctor.Ins = function (...args: any[]) {
        if (!ctor[instanceKey]) {
            ctor[instanceKey] = new target(...args);
        }
        return ctor[instanceKey]
    } as (...args: ConstructorParameters<T>) => InstanceType<T>;
    // 修改构造函数行为 
    // const original = ctor;
    // const f: any = function (...args: any[]) {
    //     if (!f[instanceKey]) {
    //         f[instanceKey] = new original(...args);
    //     }
    //     return f[instanceKey];
    // };
    // f.prototype = original.prototype;
    // retur
    return ctor;
}    

MyClass .ins() 用的时候才创建 一般是挂载在某个全局上的get方法上

分享我的单例写法:

function instDefine<T>(target: object, key: string, creator: () => T) {
    function getInst() {
        const value = creator();
        if (!value) {
            return value;
        }
        Reflect.defineProperty(target, key, {
            value,
            writable: false,
            configurable: true,
            enumerable: true,
        });
        director.on(Director.EVENT_RESET, defineGetter);
        return value;
    }

    function defineGetter() {
        Reflect.defineProperty(target, key, {
            get: getInst,
            enumerable: true,
            configurable: true,
        });
    }

    defineGetter();

    return getInst;
}

export const CCSingleton = <T>() => ({
    get base () {
        return class CCSingleton {
            protected constructor() { }
    
            static get inst() {
                const creator = () => new this() as T;
                const getInst = instDefine(this, 'inst', creator);
                return getInst();
            }
        }
    },

    from<B extends Constructor<object>>(Base: B, ...args: any[]) {
        return class CCSingleton extends Base {
            protected constructor(...args: any[]) { super(...args); }
            static get inst() {
                const creator = () => new this(...args) as T;
                const getInst = instDefine(this, 'inst', creator);
                return getInst();
            }
        }
    },
})

用法:

export class XXXMgr extends CCSingleton<XXXMgr>().base {
    hello() {
        console.log("hello, world!");
    }
}

XXXMgr.inst.hello();

export class EventCenter extents CCSingleton<EventCenter>().from(EventTarget) { }

EventCenter.inst.emit("test");

原理:

  1. 基类中定义static inst getter来创建单例实例

  2. inst getter被调用时,重新定义构造函数上的inst属性,用创建好的value替换掉原来的inst getter

  3. cocos重新启动时,把inst属性复原为inst getter

  4. 这种在定义类时把类名作为泛型参数传给基类的做法叫做CRTP模式,这个写法来自C++,目的是让基类能够使用子类(例如我代码中的as T)。

优势:

  1. 强类型,良好的编辑器提示支持

  2. 延迟创建,只有在第一次访问inst属性的时候,实例才会被创建

  3. 性能,实例创建后直接覆盖原有getter,后续访问不再有函数调用开销

  4. 灵活,可以通过from方法来指定被继承的基类

  5. 结合cocos生命周期,游戏重启时自动清除现有实例,避免内存数据遗留

我觉得单例不同的项目,不同的情况适合不同的写法,分享一下我在子游戏中的单例写法

/**
 * 单例模式基类
 */
export class Singleton extends HashObject {
    /**该对象是否可用 */
    public isValid: boolean = true;
    /**得到一个实例 */
    public static instance<T extends {}>(this: new () => T): T {
        if (!(<any>this)._instance) {
            (<any>this)._instance = new this();
            (<any>this)._instance._ctor_();
        }
        return (<any>this)._instance;
    }
    /**这里可以初始化单例需要做的一些事情 */
    protected _ctor_() {}
    /**是否已经存在的实例 */
    public static isInstance(): boolean {
        return !!(<any>this)._instance;
    }
    /**销毁单例 */
    public static destroy() {
        if (!(<any>this)._instance) return;
        (<any>this)._instance.onDestroy();
        (<any>this)._instance.isValid = false;
        delete (<any>this)._instance;
    }
    /**执行销毁函数 */
    protected onDestroy() {}
}

这个挺好我以前用的这个.楼上有人分享过.
不过我回复他我以前遇到的情况,希望看下用这种继承式有没有方式处理我的问题.
继承式需要多继承一层:

class A extends Singleton {}
export class APlus extends ACompent {}

以及依赖调用对象决定实例:

class A extends Singleton {}
class B { static Holder; } //Holder is a Function
B.Holder = A.instance // console B.Holder() => B instance

后来他删评论了, 没和我继续讨论.

继承制的确实有很大的局限性,所以我们项目中也有类装饰器的的单例创建方式。只是单纯的更喜欢aaa.ins().aa()这种使用方式 :rofl: ,如果ts支持多继承能方便很多。

ts有类似多继承的mixins模式,我前面发的写法就是用mixins实现的。

问题2有办法不.

如果你指的是inst的类型问题,我前面的代码中已经用CRTP模式解决了,inst虽然在基类(mixins)中定义,但是inst的类型却是子类决定的。

我翻了下你前面的讨论内容,你说得问题2是指实例创建卸载不可控么?我现在的写法会在监听到director的RESET事件时卸载现有实例。对于我的方案,你可以考虑让它监听其他事件来同样做卸载现有实例的事,也可以给mixins基类添加方法来手动抛弃现有实例。

说实话我以前用的就是这种类型或者变种:
class XXMgr{
}
export const xxMgr = new XXMgr();
的,现在的写法是
export default class ResManger extends Singleton {}
//Singleton.ts
export default class Singleton {
public static getInstance<T extends {}>(this: new () => T): T {
if (!(this)._instance) {
(this)._instance = new this();
}
return (this)._instance;
}
}
我其实以前就不怎么喜欢单例模式,一般而言一个项目里面本身就只有管理器脚本才需要用到单例,而不用单例也能写管理器(你不可能在多个地方写同一个管理器的初始化代码吧?)
现在写的这个不知道到底有啥好处,能用就行。

我的是这样的

/**
 * 单例基类
 * 
 * @example
 * ```typescript
 * class MyManager extends SingletonBase<MyManager>() {
 *     public doSomething(): void {
 *         // 你的实现
 *     }
 * }
 * 
 * // 使用
 * const manager = MyManager.instance;
 * manager.doSomething();
 * ```
 */
export function SingletonBase<T>() {
    class SingletonClass {
        private static _instance: SingletonClass = null;

        protected constructor() {
            const constructor = this.constructor as typeof SingletonClass;
            if (constructor._instance) {
                throw new Error(`${constructor.name} 是单例类,请使用 instance 获取实例`);
            }
        }

        public static get instance(): T {
            if (this._instance == null) {
                this._instance = new this();
            }
            return this._instance as T;
        }

        /**
         * 检查单例实例是否存在
         */
        public static get isInitialized(): boolean {
            return this._instance != null;
        }

        /**
         * 销毁单例实例
         */
        public static release(): void {
            this._instance = null;
        }
    }

    return SingletonClass;
}
import { Component, director, game, Node } from 'cc';

/**
 * 组件单例基类
 * 
 * @example
 * ```typescript
 * class MyUIManager extends SingletonComBase<MyUIManager>() {
 *     public doSomething(): void {
 *         // 你的实现
 *     }
 * }
 * 
 * // 使用
 * const manager = MyUIManager.instance;
 * manager.doSomething();
 * ```
 */
export function SingletonComBase<T>() {
    class SingletonComClass extends Component {
        private static _instance: SingletonComClass = null;

        public static get instance(): T {
            if (this._instance == null) {
                const node = new Node();
                node.name = this.name;
                node.parent = director.getScene();
                director.addPersistRootNode(node);
                this._instance = node.addComponent(this);
            }
            return this._instance as T;
        }

        /**
         * 检查单例实例是否存在
         */
        public static get isInitialized(): boolean {
            return this._instance != null;
        }

        /**
         * 销毁单例实例
         */
        public static release(): void {
            if (this._instance) {
                this._instance.node.destroy();
                this._instance = null;
            }
        }
    }
    return SingletonComClass;
}

你已经回答自己了, 你用不到高数总不能说高数没用吧.
单例按照实现难易度和实现方式来区分不都是为了应付项目场景.
我想要可控的时机, 导出式就被否掉了.
我想要不被调用对象影响的static function, 不想用晦涩的混合继承模式处理"多继承"需求, 那么继承式又被否掉了
我想要别人写在编辑自己的代码的时候不会有哪怕万分之一的概率撞变量名, 于是symbol要用上.
别人需要可动态装/卸载的单例, 那么可供装/卸载的接口又需要补充了.
诸如此类的需求很多.
我并非针对你, 只想顺道借楼说些话:

做技术只想单纯做个业务仔或用资历混工资高级程序,
做是5年10年还和刚入行那样写着含糊或者不太周全代码,
出事要么推锅要么喷人,
这样心态的人其实没必要勉强自己讨论或者回复. 
一句够用就行, 一个能跑就好, 一行keep it simple. 我看着就觉得搞笑.
我以为这些都是别人在玩抽象, 没想到还真有人用自己实践了.
1赞

讨论了这么多啊,很多人的单例比较简单的原因是:其实他们的单例只是需要一个静态方法而已,强行封装成了单例。当然,在js环境里,我也常常这么干

1赞

我也觉得一个单例能讨论这么多感到很意外 设计模式最好保持单一原则, 减少认知差异 因为设计模式本就是公共通用的一个思想, 最好保持团队内和团队外的人都能一眼上手

这个怎么在创建单例的时候传入参数啊?装饰器build的时候还不能确定需要传什么参数啊,实际是在创建单例的时候才需要传参啊?

已经确定的参数在注册的时候写入会在初始化的时候传入, 你想动态的话也可以传入function加个判断就可以了
获取的实例的时候不用选择是否传参 应该是你看错了, 你重新看下代码应该能明白