函数的单例模式
# 单例模式
一般在前端实现单例模式,大多数都会使用类去实现,因为类的实现,看起来比较简单,下面是一个简单的例子。
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 用单例会有 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 了,而这也跟类一个原理了,因为类更改的成员属性,都是实例对象本身的,而不是外部的,所以,他能更新上。这个问题,也是后面我发现的,所以以此记录一下。