2021.12.20 更新

因为最近用 vue3 新写了一个官网类型的网站,当时也是有用 vue3 的模式写一个手动挂载的登录弹窗组件,但是也是为了写的比较满意使用的单例模式来做,之后发现之前这篇博文在介绍单例模式方面的写法是没有问题的,但是在举例 vue 单例弹窗应用的时候有一些细节没有介绍到。

import Vue from 'vue'
import loginPopupComponent from './login.vue'

import i18n from '@/i18n/index.js'

const getSingle = function(fn) {
  var result
  return function() {
    return result || (result = fn.apply(this, arguments))
  }
}

const Login = {
    Vue.prototype.$loginPopup = getSingle(function(options) {
      const LoginConstructor = Vue.extend(loginPopupComponent)
      const div = document.createElement('div')
      document.body.appendChild(div)
      const vm = new LoginConstructor({
        propsData: options,
        i18n
      }).$mount(div)
      vm.innerShowPopup = true
      return vm
    })
  }

}

export default Login

如上代码,这段代码在实现单例模式后,以 this.$loginPopup() 的函数调用形式可以打开登录弹窗,但是却忽略了一个问题:当点击关闭按钮关闭了这个弹窗之后,因为返回的 vm 实例没有被销毁,再次点击打开弹窗的按钮就不能打开这个弹窗了,因此,如果要实现单例登录弹窗,还有一步比不可少的操作就是当弹窗被关闭后,必须将 vm 销毁,这样才能再次打开该弹窗。(如果觉得比较麻烦也可以不使用单例模式来设计这个登录组件,因为在一般正常的情况下,页面是不会出现多个登录弹窗显示的)

那么在 vue3 项目中,笔者是这样实现这个单例登录组件的:

import { createVNode, render } from 'vue-demi'
import LoginConstructor from './index.vue'

// 登录弹窗单例模式
const login = (function () {
  let vm
  let container = document.createElement('div')
  return function (options = { modelValue: true }) {
    if (!vm) {
      const props = {
        ...options
      }

      document.body.appendChild(container)

      vm = createVNode(LoginConstructor, props)

      // console.log('vm', vm)

      render(vm, container)
    }
    if (options.destory === true) {
      vm = null
      container && render(null, container) // 清空 dom
    }
    return vm
  }
})()

export default login

 如上代码,这里给 options 对象一个 destory 的属性,这样在要关闭弹窗时可调用改方法传入 destory 为 true, 这样 vm 便被销毁了,再次点击按钮就可以正常生成新的弹窗,这样做的好处是不管在什么情况下一次最多都只能生成一个登录弹窗,实现了单例模式。

import app from '@/main.js'

watch(
        () => state.visible,
        (nVal) => {
          instance.emit('update:modelValue', nVal)

          // 关闭弹窗后需要销毁实例(使登录弹窗处于单例下)
          if (nVal === false) {
            app.config.globalProperties.$loginPopup({ destory: true })
          }
        }
      )

如上,在组件内部利用 watch 监听弹窗是否展示变量 visible ,当 visible 为 false 时传入 destory 为 true 销毁实例。由于是 vue3 项目在一些具体的写法方面和 vue2 略有不同,道理都是一样的。

---------------------------------------------------------------

前言

在之前笔者发布的一篇文章中(Vue全局手动挂载组件封装(Message, Loading, Spin类组件))有介绍到在 vue 中实现全局手动挂载登录组件的实现,这样做的好处是可以在任何页面的业务逻辑中轻松调用一行代码便拉起了登录弹窗(this.$loginPopup())。但是当时在设计的时候还有一个问题没有考虑就是这个登录弹窗组件的调用生成方法并不是单例的,这就意味着在某些时候可能在页面上会同时弹出好几个一样的登录弹窗,而在我们的业务逻辑中一般认为登录弹窗是唯一的;因此需要对这段组件的生成代码进行单例模式的重写,顺便认识一下 JS 中的单例设计实现。

1. 关于单例模式

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

其实在面试中偶尔也会问到,一般面试官可能会让我们说出几个常见的设计模式,比如说单例模式、代理模式、适配器模式、发布-订阅模式等等。那么什么是单例模式呢?单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。比如说 vue 中的 vuex 中的 store 对象就用到了单例模式,因为 vuex 中的 store 对象是供所有的组件所共享的,因此需要一个唯一的对象来保存这些数据。

2. 简单的单例模式实现

var Singleton = function( name ){ 
 this.name = name; 
 this.instance = null; 
}; 

Singleton.prototype.getName = function(){ 
 alert ( this.name ); 
}; 

Singleton.getInstance = function( name ){ 
 if ( !this.instance ){ 
 this.instance = new Singleton( name ); 
 } 
 return this.instance; 
}; 

var a = Singleton.getInstance( 'sven1' ); 
var b = Singleton.getInstance( 'sven2' ); 
alert ( a === b ); // true

 这是来自《JS设计模式与开发实践》一书中的 JS 实现的单例模式示例,这里定义了一个单例类(函数)Singleton, 并在该类上定义了静态方法 getInstance, 当需要生成唯一的实例的时候便可通过调用 getInstance 生成,代码具体思路也很易懂,先判断是否已生成了 Singleton 类的实例 instance, 若 instance 已存在则直接返回 instance, 否则通过 new Singleton(name) 的方式来生成 instance 实例。这种代码思路,不仅是 js 也是其他所有程序语言实现单例的基础思路。而对于 JS 等拥有闭包特性的语言还可以通过闭包实现单例模式:

var Singleton = function( name ){ 
 this.name = name; 
}; 
Singleton.prototype.getName = function(){ 
 alert ( this.name ); 
}; 
Singleton.getInstance = (function(){ 
 var instance = null; 
 return function( name ){ 
 if ( !instance ){ 
 instance = new Singleton( name ); 
 } 
 return instance; 
 } 
})();

如上,我们通过闭包来保存 instance 对象,使其无法在 getInstance 方法被调用完毕后被垃圾回收掉,这样在重复调用 getInstance 方法后会因为已经存在 instance 实例而将其返回。关于闭包的深入理解可以参考这篇文章 深入理解javascript原型和闭包(15)——闭包 - 王福朋 - 博客园,写的很细。

3. 在 vue 中实现单例登录弹窗组件

可先阅读这篇文章:Vue全局手动挂载组件封装(Message, Loading, Spin类组件)_似水流年的博客-CSDN博客_vue挂载全局组件 。如该文章所述,我们通过 vue 提供的 Vue.extend 方法将引入的登录组件作为参数生成了一个构造函数,该构造函数的参数便是登录组件的 props 及一些其他 vue 组件相关的参数(如 i18n 等)。代码如下:

import Vue from 'vue'
import loginPopupComponent from './login.vue'
 
import i18n from '@/i18n/index.js'
 
 
const Login = {
  install (Vue, options) {
    Vue.prototype.$loginPopup = function (options) {
      const LoginConstructor = Vue.extend(loginPopupComponent)
      const div = document.createElement('div')
      document.body.appendChild(div)
      const vm = new LoginConstructor({
        propsData: options,
        i18n
      }).$mount(div)
      vm.innerShowPopup = true
    }
  }
}
 
export default Login

那么在全局注册之后实际调用时:

this.$loginPopup() 

 便可生成登录弹窗。但是,这样生成的弹窗并不是单例的,比如我们如果重复调用 $loginPopup 方法:

this.$loginPopup()
this.$loginPopup()
this.$loginPopup()

 便会出现一下情况:

显然这不是我们想要的结果。因此引入单例模式很有必要,于是有了这样的改写:

Vue.prototype.$loginPopup = (function(options) {
      let vm
      return function(options) {
        if (!vm) {
          const LoginConstructor = Vue.extend(loginPopupComponent)
          const div = document.createElement('div')
          document.body.appendChild(div)
          vm = new LoginConstructor({
            propsData: options,
            i18n
          }).$mount(div)
          vm.innerShowPopup = true
        }
        return vm
      }
    })()

 如上,我们通过将原来的 $loginPopup 函数放入函数中进行返回的方式形成了一个闭包,在该函数中返回登录组件实例 vm,通过判断 vm 是否存在来决定是否还要生成登录弹窗组件。于是再重复多次调用 $loginPopup 方法也只会生成一个登录弹窗了。

4. 再次重构

虽然已经实现了单例登录弹窗组件,但是刚才的那种写法仍然不够好,因为它违背了函数设计的单一职责原则,即一个函数做了两件事情:1. 生成了登录弹窗;2. 使用了单例模式,代码耦合性较强。那么我们接下来就将其拆分出来。完整代码如下:

import Vue from 'vue'
import loginPopupComponent from './login.vue'

import i18n from '@/i18n/index.js'

const getSingle = function(fn) {
  var result
  return function() {
    return result || (result = fn.apply(this, arguments))
  }
}

const Login = {
    Vue.prototype.$loginPopup = getSingle(function(options) {
      const LoginConstructor = Vue.extend(loginPopupComponent)
      const div = document.createElement('div')
      document.body.appendChild(div)
      const vm = new LoginConstructor({
        propsData: options,
        i18n
      }).$mount(div)
      vm.innerShowPopup = true
      return vm
    })
  }

}

export default Login

我们将实现单例模式的代码封装为 getSingle 函数(有点类似于防抖节流函数),其参数为需要进行单例控制的函数,通过 apply 方法改变 this 指向来执行 fn 函数,实现了单例模式函数和登录弹窗生成函数的拆分,并且 getSingle 也可以用在其他需要进行单例控制的地方,通用性较强。

5. 总结

单例模式其实有饿汉和懒汉之分,一般认为懒汉式是更合适的单例模式,当然业务场景不同也是都有不同的使用场景的。在我们上文中介绍的单例模式都是懒汉式的单例,即需要调用对象生成时才去生成需要的单例对象。JS 中的单例模式实现除了传统思路外还有可以利用闭包特性实现,通过闭包实现可以降低代码的耦合性,当然需要先理解闭包的含义,而理解闭包,则需要先理解 JS 的作用域问题,顺着这个思路可以了解到大量的 JS 高级特性,大有管中窥豹,可见一斑之势。

参考:

曾探 《JavaScript 设计模式与开发实践》

JavaScript 设计模式 - 单例模式_Vuex

设计模式 | 菜鸟教程

深入理解javascript原型和闭包(15)——闭包 - 王福朋 - 博客园

Logo

前往低代码交流专区

更多推荐