函数实现单例模式

285 阅读3分钟

wallhaven-gpqye7.jpg

单例模式

一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。

class Foo {
  static instance;
  static init() {
    if (!this.instance) this.instance = new Foo();
    return this.instance;
  }
  constructor() {}
}

// 将单例实例化 并暴露出去
export default Foo.init()

如此,我们就实现了简单的单例模式,并且在其他文件引入的时候已经是实例化过一次的了,或者交由用户者自行调用 init 也是可以的

函数实现

而在函数的实现上,其实本身类就是函数的某种抽象,如果去掉这个 new 的话,单纯用函数又是怎么做的呢?

let ipcMainInstance;
export default () => {
  const init = () => {
    return {
      name: "phy",
      hobby: "play games"
    };
  };

  return () => {
    if (!ipcMainInstance) {
      ipcMainInstance = init();
    }
    return ipcMainInstance;
  };
};

使用

const ipcInit = createIpc();
ipcInit();

因为我们使用的是二阶函数进行 init,所以写法上是二次调用才是 init,每个人的设计写法不一样。

然而这种写法上,每次都要写一个 init 方法进行单例实例化的包裹,这明显是一个重复工作,我们是否可以将 init 方法独立成一个函子,让他帮我们自动将我们传进去的函数进行处理,返回来的就是一个单例模式的函数呢?

抽象单例模式函子

// 非void返回值
type NonVoidReturn<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R extends void
    ? never
    : T
  : any;

/**
 * 创建单例模式的函子
 * @param {function} fn
 * @returns {any} fn调用的返回值 必须得有return 可推断
 */
const createSgp = <T extends (...args: any) => any>(fn: NonVoidReturn<T>) => {
  let _instance: undefined | ReturnType<T>;

  return () => {
    if (!_instance) {
      _instance = fn();
    }
    return _instance;
  };
};

export default createSgp;

使用上

import createSgp from "./createSgp";

const useAuto = () => {
  let count = 0;

  const setCount = (num: number) => {
    count = num;
  };
  
  const getCount = () => count
  
  return {
    getCount,
    setCount
  };
};

// 将其处理成单例模式 并且暴露出去
export default createSgp(useAuto);

如此我们就完成了单例模式的包裹处理,并且是一个单例模式的函数。

对于hooks使用单例模式函数的问题

其实上面的操作看起来很酷,实际上很少会用到,因为你得考虑到,我用单例模式的意义是什么,如果这个函数只需要调用一次,那么就有必要用单例模式,但是hooks一般用到的时候,都属于操作性逻辑,尽量不应在hooks里面去做hooks初始化时有函数自执行调用,这个调用应该交由用户去做,我是这么理解hooks的,而这也就导致,hooks不应该用单例了,而且hooks用单例会有bug,请看下面的代码:

  let count = 0;
  const useCount = {
    count,
    add(num){
      count += num
    }
  }

这里我就一次简化useCount的return出来的东西,那么我们思考下,如果说,这个add在外部调用了,那么这个count会变吗?答案是不会,为什么呢?

因为当前add操作的count,是外部的count,并不是return对象的count,这句话可能很绕,但是仔细思考,一开始useCount(),他return的count是长什么样,此时,他其实就是数字0,那么,add改的count真的是这个return对象的count吗?相信说到这里,你就懂为什么了。

那我如果真的要联动到这个count,怎么做呢?

  const useCount = {
    count: 0,
    add(num){
      this.count += num
    }
  }

答案是,用到this,此时这个add操作的count就是此时return 对象的count了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。