這裏還是以element-ui爲例,那我們就看看裏面的Message。

它的dom結構什麼的就寫在node-modules/element-ui/packages/notification/src/main.vue裏面

<template>
  <transition name="el-notification-fade">
    <div
      :class="['el-notification', customClass, horizontalClass]"
      v-show="visible"
      :style="positionStyle"
      @mouseenter="clearTimer()"
      @mouseleave="startTimer()"
      @click="click"
      role="alert"
    >
      <i
        class="el-notification__icon"
        :class="[ typeClass, iconClass ]"
        v-if="type || iconClass">
      </i>
      <div class="el-notification__group" :class="{ 'is-with-icon': typeClass || iconClass }">
        <h2 class="el-notification__title" v-text="title"></h2>
        <div class="el-notification__content" v-show="message">
          <slot>
            <p v-if="!dangerouslyUseHTMLString">{{ message }}</p>
            <p v-else v-html="message"></p>
          </slot>
        </div>
        <div
          class="el-notification__closeBtn el-icon-close"
          v-if="showClose"
          @click.stop="close"></div>
      </div>
    </div>
  </transition>
</template>

可以看出,這裏東西不多,只有一些對應的class、關閉按鈕外,就剩下title和message了,這裏看到用了slot,所以我們通常也只能將自己的自定義內容放入此插槽中。

js方便就是支持大家常用的點擊、關閉回調,裏面維護了一個計時器來控制關閉。

我們能將它作爲命令式只用的主要實現邏輯是在node-modules/element-ui/packages/notification/src/main.js裏面。

這裏的核心是Notification方法,這個包導出的也是它。

我們先看大家常用的 this.$message.success 方法是什麼

['success', 'warning', 'info', 'error'].forEach(type => {
  Notification[type] = options => {
    if (typeof options === 'string' || isVNode(options)) {
      options = {
        message: options
      };
    }
    options.type = type;
    return Notification(options);
  };
});

通過這裏我們可以得知,原來平時使用的 this.$message.success 其實就是 Notification.success ,本質就是一個預先設定了 type 後再調用了 Notification 方法。

let instance;
let instances = [];
let seed = 1;

const Notification = function(options) {
  if (Vue.prototype.$isServer) return;
  options = merge({}, options);
  const userOnClose = options.onClose;
  const id = 'notification_' + seed++;
  const position = options.position || 'top-right';

  options.onClose = function() {
    Notification.close(id, userOnClose);
  };

  instance = new NotificationConstructor({
    data: options
  });

  if (isVNode(options.message)) {
    instance.$slots.default = [options.message];
    options.message = 'REPLACED_BY_VNODE';
  }
  instance.id = id;
  instance.$mount();
  document.body.appendChild(instance.$el);
  instance.visible = true;
  instance.dom = instance.$el;
  instance.dom.style.zIndex = PopupManager.nextZIndex();

  let verticalOffset = options.offset || 0;
  instances.filter(item => item.position === position).forEach(item => {
    verticalOffset += item.$el.offsetHeight + 16;
  });
  verticalOffset += 16;
  instance.verticalOffset = verticalOffset;
  instances.push(instance);
  return instance;
};

上面的 NotificationConstructor 就是

import Main from './main.vue';
const NotificationConstructor = Vue.extend(Main);

使用Vue構造函數生成組件實例 instance ,這和我們默認用vue-cli生成vue項目的main.js是不是很像。

import App from './app.vue';
new Vue({
  render: h => h(App),
  router,
  store: new Vuex.Store(store),
}).$mount("#app");

唯一的區別是我們在 mounted 的時候沒有傳參數,這意味着我們需要自己將生成的dom插入目標位置,這也更符合我們的預期。

instances 數組是用來存儲多個組件實例的,在垂直方向如果有多個組件實例的話,會給它們直接加入16px的偏移量避免它們重疊在一起。

這裏還給每個實例更新了 verticalOffset 屬性的值來記錄垂直方向的偏移量,方便 positionStyle 變更,更新組件的位置。

我們同樣可以看到,在移除組件的時候,需要同步變更 instances 數組裏面該 instance 後面的實例對應的style樣式。

Notification.close = function(id, userOnClose) {
  let index = -1;
  const len = instances.length;
  const instance = instances.filter((instance, i) => {
    if (instance.id === id) {
      index = i;
      return true;
    }
    return false;
  })[0];
  if (!instance) return;

  if (typeof userOnClose === 'function') {
    userOnClose(instance);
  }
  instances.splice(index, 1);

  if (len <= 1) return;
  const position = instance.position;
  const removedHeight = instance.dom.offsetHeight;
  for (let i = index; i < len - 1 ; i++) {
    if (instances[i].position === position) {
      instances[i].dom.style[instance.verticalProperty] =
        parseInt(instances[i].dom.style[instance.verticalProperty], 10) - removedHeight - 16 + 'px';
    }
  }
};

順便提一下, PopupManager 是element-ui裏面公共的通過z-index來確保最新創建的dom永遠保持在最上方不被舊的元素遮擋的機制,實現原理也很簡單。

鑑於message傳參的options.message支持傳入vNode,所以會有針對vNode相關的判斷和處理邏輯。

簡單總結一下,使用Vue構造函數這種方法是常用的命令式組件的套路,類似的還有Dialog(有的組件庫叫Modal)、Notice等。

相關文章