Skip to content

假设我们有一个 Vue 单文件组件 Toast.vue, 我们不想通过 <Toast /> 的方式来使用它,而是想通过 this.$toast() 的方式来使用它。

那么也就是说 this.$toast() 触发时,我们需要手动创建一个 Toast 组件实例,并将其挂载到目标元素下。

如果我们想要实现上述功能,就需要用到 Vue.extend

但在此之前,我们需要了解 Vue 实例的 $el$mount 方法。

16-1.$el

$elVue 实例的一个属性,表示当前 Vue 实例使用的根 DOM 元素

有两个注意点:

  1. 如果 Vue 实例在实例化时没有声明 el 选项,则它处于“未挂载”状态,vm.$elundefined
  2. 如果 Vue 实例在实例化没有声明 el 选项,且调用 vm.$mount() 方法时没有提供 elementOrSelector 参数,则 vm.$elundefined

16-2.$mount

基础语法如下:

js
vm.$mount([elementOrSelector])

实现一个组件实例的挂载,一共有以下 3 种方式:

  1. 在组件实例化时传入 el 选项。

  2. 使用 vm.$mount([elementOrSelector]) 手动地挂载一个未挂载的实例。

  3. 如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的元素,那么我们可以使用原生 DOM API 把它插入文档中。

js
// 1
const vm = new Vue({
  el: '#app'
})

// 2
const vm = new Vue()
vm.$mount('#app')

// 3
const vm = new Vue()
vm.$mount()
document.body.appendChild(vm.$el)

16-3.Vue.extend

Vue.extend 用于创建一个 Vue 组件的子类。它返回的是一个扩展实例构造器。

基本语法如下:

js
const SubConstructor = Vue.extend(options)

const subInstance = new SubConstructor()

subInstance.$mount()

document.body.appendChild(subInstance.$el)

现在,以开头所述为例。假设 Toast.vue 组件的 vue 单文件内容如下:

vue
<template>
  <transition name="fade" @after-leave="handleAfterLeave">
    <div class="toast" v-show="visible">
      <span>{{ message }}</span>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'Toast',
  props: {
    autoClose: {
      type: Boolean,
      default: true
    },
    duration: {
      type: Number,
      default: 3000
    },
    message: {
      type: [String, Number],
      default: ''
    }
  },
  data () {
    return {
      visible: false
    }
  },
  mounted () {
    // 需要自动关闭时,调用startTimer
    if (this.autoClose) this.startTimer()
  },
  beforeDestroy () {
    this.stopTimer()
  },
  methods: {
    startTimer () {
      this.visible = true
      if (this.duration > 0) {
        this.timer = setTimeout(() => {
          this.visible = false
          // this.$nextTick(() => {
          //   this.$el.parentNode.removeChild(this.$el)
          // })
        }, this.duration)
      }
    },
    stopTimer () {
      if (this.timer) clearTimeout(this.timer)
    },
    // 动画结束之后 如果在定时器结束时removeChild,那么将不会触发leave动画
    handleAfterLeave () {
      // 触发 beforeDestroy 和 destroyed 的钩子
      this.$destroy(true)
      this.$el.parentNode.removeChild(this.$el)
    }
  }
}
</script>

<style lang='less' scoped>
.toast {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0,0,0,0.85);
  border-radius: 10px;
  font-size: 14px;
  color: #FFFFFF;
  line-height: 14px;
  padding: 16px 46px;
  white-space: nowrap;
  text-align: center;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
  transform: translateY(calc(-50% - 50px)) translateX(-50%);
}
.fade-enter-to, .fade-leave {
  opacity: 1;
  transform: translateY(-50%) translateX(-50%);
}
.fade-enter-active, .fade-leave-active {
  transition: all ease .5s;
}
@media screen and (max-width: 680px) {
  border-radius: 4px;
  font-size: 12px;
  line-height: 12px;
  padding: 14px 30px;
}
</style>

那么我们可以通过 Vue.extend 来创建一个 Toast 组件的子类,并通过函数手动挂载它:

js
import Vue from 'vue'
import Toast from './Toast'

Vue.prototype.$toast = (options = {}) => {
  generateInstance(options)
}

const ToastConstructor = Vue.extend(Toast)

let zIndex = 9999

function generateInstance (options) {
  options = typeof options === 'object' ? options : {
    message: String(options)
  }
  const instance = new ToastConstructor({
    propsData: options
  })
  const { appendToElement, top, left } = options
  // 父级元素
  const mountTarget = appendToElement || document.querySelector('body')
  const { position } = window.getComputedStyle(mountTarget)
  if (position !== 'fixed' && position !== 'absolute') {
    mountTarget.style.position = 'relative'
  }
  // 挂载
  instance.$mount()
  const dom = instance.$el
  // 支持更改定位
  if (appendToElement) {
    dom.style.position = 'absolute'
  } else {
    dom.style.position = 'fixed'
  }
  typeof top !== 'undefined' && (dom.style.top = top)
  typeof left !== 'undefined' && (dom.style.top = left)
  dom.style.zIndex = zIndex
  zIndex++
  // 插入
  mountTarget.appendChild(dom)
  return instance
}

TIP

唯一要注意的是,上述实例化使用到了propsData 选项,它用于传递实际的 props 数据。

可以看做组件实例化时的自定义入参