Skip to content

单例模式在Dialog类组件中的使用 #3

@SoloJiang

Description

@SoloJiang

单例模式

单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

场景

单例模式的优点主要体现在节省内存,始终只使用一个实例,所用到的场景也往往是,虽然实例可能会被多次使用,但每次使用都是独立发生的,而不是同时进行的。

使用方法

通常来说,我们在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 的组件化给了我们很多种写法:

  1. 写好一个可以用 slot 插入的弹窗组件,然后在每一个要调用的组件处用引入,例如:

    <template>
        <div>
            <my-dialog :show.sync="showDialog">
                <div slot="content">
                    ...
                </div>
            </my-dialog>
        </div>
    </template>

    这样做,可以通过一个双向绑定的变量来控制弹窗的显隐。但是,每一次的引用让我们的代码非常的冗余,甚至是繁琐。

  2. 既然上一种方法那么繁琐,我们试试看新的方法?

    想到了类似于 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)由于我们的场景中存在对话框形式的弹窗又或者是提示类弹窗,可配置性太差。

  3. 比较好的做法是,使用单例模式,也就是说当我们已经在 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这类会在全局中显示,并且在不同组件中都会去频繁唤起的场景中。因为这样可以让我们更加方便的调用,甚至性能更好。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions