-
Notifications
You must be signed in to change notification settings - Fork 0
Description
单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
场景
单例模式的优点主要体现在节省内存,始终只使用一个实例,所用到的场景也往往是,虽然实例可能会被多次使用,但每次使用都是独立发生的,而不是同时进行的。
使用方法
通常来说,我们在javascript想去使用单例模式的做法是:
let createObject = function(params) {
let instance = new target(params)
return instance
}
let getSingle = function(fn) {
let result
return function(...args) {
return result || (result = fn.apply(this, args))
}
}我们通过闭包对实例做了缓存,当我下一次调用这个新建对象的函数时,会先检查缓存里有没有这个实例,有的话就直接返回,没有的话就创建一个新的实例。
但是,这段代码其实并不能满足我们所有的需求,因为,假如我们实例化的对象在每次调用 create 的时候需要初始化的参数不同。然而,在这个地方,其实我们每次都是直接使用之前缓存的对象,而没有做初始参数替换。
所以,在此,我们以创建一个 div 为例,我们只需要创建一个 div,但是当我们在不同地方使用这个 div 的时候,很有可能只是希望改一改这个 div 里面的内容就好了。后面会谈到,这篇文章的主题——用单例模式来设计一个 Dialog 类。这里我们先来实现这个例子,如果,需要每次都更新这个类的参数,那么我们就需要一个专门函数来更新这些参数。
代码做如下改动:
const createLayer = function(html) {
const div = document.createElement('div')
// 填充参数
fillContent(div, html)
document.body.appendChild(div)
return div
}
// 此处根据具体需求去写填充参数的函数
const fillContent = (layer, html) => {
layer.innerHTML = html
}
const getSingle = function(fn) {
let result
return function(...args) {
if (result) {
// 假如实例存在,只需要去更新参数
fillContent(result, ...args)
return result
} else {
return result = fn.apply(this, args)
}
}
}
const createSingleLayer = getSingle(createLayer)
var a = createSingleLayer("aaa")
var b = createSingleLayer("bbb")
console.log(a === b) // true以上代码,所做的就是 a, b 使用了同一个 div,也就是使用的同一个 dom 结构,只是做了其中内容的替换。
Vue 中使用单例模式创建一个 DialogContainer 类
我们通常会使用一个弹窗,Vue 的组件化给了我们很多种写法:
-
写好一个可以用 slot 插入的弹窗组件,然后在每一个要调用的组件处用引入,例如:
<template> <div> <my-dialog :show.sync="showDialog"> <div slot="content"> ... </div> </my-dialog> </div> </template>
这样做,可以通过一个双向绑定的变量来控制弹窗的显隐。但是,每一次的引用让我们的代码非常的冗余,甚至是繁琐。
-
既然上一种方法那么繁琐,我们试试看新的方法?
想到了类似于 ElementUI 中的使用方法,通过
this.$message(options)这种方法来调用弹窗,例如:/** * 目录结构: * Message/ * Message.vue * index.js */ // Message.vue,省略,就和正常的组件写得一样就行 ... // index.js import Vue from 'vue' // 得到一个普通的对象,其中包含的属性是你在 Message.vue 中导出的属性以及一些和 render 有关的属性或方法 import Message from './Message.vue' Message.installMessage = function(options) { // 此处的 options 就是 this.$message(options) 所传入的形参 if (options === undefined || options === null) { options = { message: '' } } else if (typeof options === 'string' || typeof options === 'number') { options = { message: options } } // 将其拓展成一个 VueComponent 的构造函数,这样它的原型上就拥有了 VueComponent 的方法,这里的 extend 很类似于 jQuery 中的 extend var message = Vue.extend(Message) // 实例化成一个 VueComponent 对象,并且调用 $mount 将其转化成 虚拟dom var component = new message({ data: options }).$mount() document.querySelector('body').appendChild(component.$el) } export default { install: Vue => { Vue.prototype.$message = Message.installMessage } }
然后在项目入口处
Vue.use(message)之后就可以通过this.$message(options)这种方法去直接调用并显示弹窗了。但是这种写法仍然不够完美。主要原因是:(1) 每次调用都会往 body 中插入一个新的dom元素;(2)由于我们的场景中存在对话框形式的弹窗又或者是提示类弹窗,可配置性太差。 -
比较好的做法是,使用单例模式,也就是说当我们已经在 body 中插入一个 Dialog 时,就不再新建它了,而是直接使用,然后我们需要做的是,让这个调用可配置,在第二种方法里我们相当于配置的是 组件的
data,那么如果我们改成配置组件的子组件呢?具体做法如下:/** * 目录结构: * DialogContainer/ * DialogContainer.vue * index.js */ // index.js import Vue from 'vue' import Container from './Container' Container.installSlot = (function() { // 单例判断 let component return function(slot) { // 判断传入的是否是一个组件 if ( typeof slot === 'object' && typeof slot.render === 'function' && !component ) { const container = Vue.extend(Container) component = new container({ components: { 'slot-component': slot } }).$mount() document.querySelector('body').appendChild(component.$el) } } })() export default { install(Vue) { Vue.prototype.$dialog = Container.installSlot } }
这样还没做完,刚才说过,在单例模式中,假如我后面调用的时候需要改变参数,怎么办呢?
Container.installSlot = (function() { // 单例判断,缓存组件以及子组件 let component, olderSlot return function(slot) { // 判断传入的是否是一个组件 if (typeof slot === 'object' && typeof slot.render === 'function') { if (!component) { const container = Vue.extend(Container) component = new container({ components: { 'slot-component': slot } }) olderSlot = slot document.querySelector('body').appendChild(component.$mount().$el) } else if (slot !== olderSlot) { // 替换 slot component.$options.components['slot-component'] = slot // 生成新的 虚拟dom,并触发视图更新 component.$mount() } } } })()
这样我们就可以灵活的通过
this.$dialog(slot)去直接使用组件,并且是单例的,只存在一个DialogContainer容器,每次调用只是去替换了其包含的组件。总结
这个容器组件使用的场景应该是什么?我觉得应该作为
Message,Dialog这类会在全局中显示,并且在不同组件中都会去频繁唤起的场景中。因为这样可以让我们更加方便的调用,甚至性能更好。