React与js设计模式:单例模式还有存在的必要吗?

1,916 阅读5分钟

设计模式是锤子,但是不能有了锤子,就看什么都是钉子。

现在对于编程设计模式的介绍,基本都基于面向对象开发方式。js虽然可以以面向对象的方式开发,但在结合react开发的过程中,现在以hooks模式的函数式编程更为主流,这使得设计模式在react中有时难以找到应用场景。找到设计模式的使用场景可能比学会设计模式的内容更有意义,不然学了也是无用武之地的屠龙术。

什么是单例模式?

单例单例,这个例指的是实例。实例哪里来,类的实例化中来。不过在js中如果不用面向对象的编程方式的话,这个单例就应该定义为函数的输出。毕竟实例对象也能通过函数得出,用工厂函数或者构造函数实例化都行。

所以单例模式可以狭义一点地定义为:某个函数执行一次或多次返回的结果都一样。这个一样是===恒等符的一样。一般而言返回是一个对象,这个对象之中都是第一次返回的那个。

单例模式可能是最简单的设计模式,基本实现流程如下图所示。

screenshot-20230501-192856.png

即函数如果执行过,就一直返回第一次执行的结果。

js单例模式通用使用方法

设计模式的关键在于功能与解耦兼得,所以我们需要一个通用的获取单例的方式,不用具体去实现某种单例。用高级函数可以很好地实现这一点:

const getSingle = (fn) => {
  var result;
  return (...arg) => {
    return result ?? (result = fn(...arg));
  };
};

实践一下:

const getCounter = () => {
  return {
    count: 0,
    increment: () => {
      this.count += 1
    },
    decrement: () => {
      this.count -= 1
    },
    getCounter: () => {
      return this.count;
    }
  }
}

const counter1 = getCounter();
const counter2 = getCounter();

console.log("counter1 === counter2: ", counter1 === counter2);
//output: counter1 === counter2:  false

const singleGetCounter = getSingle(getCounter);

const counter3 = singleGetCounter();
const counter4 = singleGetCounter();
console.log("counter3 === counter4: ", counter3 === counter4);
// counter3 === counter4:  true

react开发中,单例模式还存在吗?

上面所举的例子,是为了单例而单例,实际上我们也不会去这么用,并且在js编程中,好像我们确实没怎么用过单例模式。

单例模式的特点是产物全局唯一,其应用基本两类:

  1. 全局唯一的变量(公共状态)

  2. 需要惰性加载的全局唯一的页面元素 (登录弹窗)。

对于全局唯一变量:

但实际上我们在react或是js前端编程中好像不会特别注意什么是全局唯一的,因为全局唯一在js中是一件很容易做到的事。

const getCounter = () => {
  return {
    count: 0,
    increment: () => {
      this.count += 1
    },
    decrement: () => {
      this.count -= 1
    },
    getCounter: () => {
      return this.count;
    }
  }
}

const counter = getCount();

export { counter };

我们在其他地方引用这个类,都是全局唯一的。在面向对象的语言中不能直接创造对象,所以整出了个单例模式,但是在js中这个问题是不存在的。

此外,react中也有专门处理全局状态的redux,所以说为了全局唯一变量在js中使用单例模式的场景基本是不存在的。

惰性加载 + 全局唯一的页面元素:

就以登录弹窗为例吧:1. 弹窗不用的时候不加载,点了登录才出现,是为惰性加载。2. 这玩意全局就一个,这样的话写了东西也不会丢,用个display:none就把弹窗隐藏了。确实是个比较好的应用场景,在这两个场景下确实可以只能用单例来实现。

不过在用react开发的时候, 一般用Modal来实现登录弹窗,在semi组件库或antd中都有api可以控制弹窗是否关闭后销毁,这样就很容易做到全局唯一,我们只要在全局能拿到控制弹窗开关的变量即可。semi中还可以控制modal惰性加载,基本可以实现上面单例做到的所有事情,并且动画什么的也不用操心了。

image

结合Modal使用的话,惰性加载的全局唯一的弹窗就实现了,也就不用自己去操心单例模式了。

虽然日常开发不怎么会注意这些较为微小的性能点,以后顺手加上也是不错的。

还有一种应用场景,就是惰性计算。加入页面中有某个函数操作会触发一个十分耗时的计算,且这个计算是没有参数的,也就是当前页面上怎么算都是一个结果。这个可以用单例包一下这个函数,触发的时候再实行,用单例缓存这个结果。场景比较极端,其实很少用到。一般用个useMemo包一下,是一个较为常规的操作。

如果函数有参数的话,就考虑用个对象缓存一下结果。更谨慎的话,用缓存可以考虑用map实现先入先出缓存,防止内存泄露。

总结

  1. 单例模式很简单,但找到的react开发中的应用场景确不多。

  2. 全局变量用不上单例。

  3. 弹窗类直接用相应组件库属性更佳。

  4. 惰性计算与缓存可以用单例,不过场景比较狭窄。

  5. 一家之言,欢迎批评。