【分析element-ui源码】message组件篇

1,112 阅读2分钟

【分析element-ui源码】message组件篇

前言:刚开始 Element-ui 的源码学习不久,如果有分析不对的地方,欢迎大家留言指出
知识点:
  • hasOwnProperty
  1. hasOwnProperty指示对象自身属性中是否具有指定的属性,判断一个属性是定义在对象本身而不是继承自原型链( mdn上相关介绍:developer.mozilla.org/zh-CN/docs/…)

  2. 为什么要用 use Object.prototype.hasOwnProperty.call(myObj, prop) 代替myObj.hasOwnProperty(prop)?

    obj.hasOwnProperty(prop)可以用来判断是否具有这个属性

    如:

    o = new Object();
    o.prop = 'exists';
    o.hasOwnProperty('prop');             // 返回 true
    o.hasOwnProperty('toString');         // 返回 false
    o.hasOwnProperty('hasOwnProperty');   // 返回 false
    

    使用hasOwnProperty作为某个对象的属性名

    因为javaScript没有将hasOwnProperty作为一个敏感词,所以我们很有可能将对象的一个属性命名为hasOwnProperty,这样一来就无法再使用对象原型的 hasOwnProperty 方法来判断属性是否是来自原型链。

    var foo = {
        hasOwnProperty: function() {
            return false;
        },
        bar: 'Here be dragons'
    };
     
    foo.hasOwnProperty('bar'); // 始终返回 false
    

    不能使用 该对象.hasOwnProperty 这种方法,怎么来解决这个问题呢?我们需要使用原型链上真正的 hasOwnProperty 方法:

    ({}).hasOwnProperty.call(foo, 'bar'); // true
    // 或者:
    Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
    

    element-ui 源码:

    //vdom.js
    export function isVNode(node) {
      return node !== null && typeof node === 'object' && hasOwn(node, 'componentOptions');
    }
    
    // util.js 
    const hasOwnProperty = Object.prototype.hasOwnProperty;
    export function hasOwn(obj, key) {
      return hasOwnProperty.call(obj, key);
    }
    
  • Vue.extend:

    Vue.extend 返回的是一个“扩展实例构造器”,也就是预设了部分选项的Vue实例构造器。经常服务于Vue.component用来生成组件,可以简单理解为当在模板中遇到该组件名称作为标签的自定义元素时,会自动调用“扩展实例构造器”来生产组件实例,并挂载到自定义元素上。

源码:

main.vue组件部分

<template>
  <transition name="el-message-fade" @after-leave="handleAfterLeave">
    <div
      :class="[
        'el-message',
        type && !iconClass ? `el-message--${ type }` : '',
        center ? 'is-center' : '',
        showClose ? 'is-closable' : '',
        customClass
      ]"
      :style="positionStyle"
      v-show="visible"
      @mouseenter="clearTimer"
      @mouseleave="startTimer"
      role="alert">
      <i :class="iconClass" v-if="iconClass"></i>
      <i :class="typeClass" v-else></i>
      <slot>
        <p v-if="!dangerouslyUseHTMLString" class="el-message__content">{{ message }}</p>
        <p v-else v-html="message" class="el-message__content"></p>
      </slot>
      <i v-if="showClose" class="el-message__closeBtn el-icon-close" @click="close"></i>
    </div>
  </transition>
</template>

<script>
  const typeMap = {
    success: 'success',
    info: 'info',
    warning: 'warning',
    error: 'error'
  };

  export default {
    data() {
      return {
        visible: false,  //显示隐藏
        message: '',  //消息文字
        duration: 3000,  //显示时间, 毫秒。设为 0 则不会自动关闭
        type: 'info',  //主题
        iconClass: '',  //自定义图标的类名,会覆盖 type
        customClass: '',  //自定义类名
        onClose: null,  //关闭回调函数
        showClose: false, //是否显示关闭按钮
        closed: false, //是否关闭
        verticalOffset: 20,  //距离顶部距离
        timer: null,  //定时器
        dangerouslyUseHTMLString: false, //是否将 message 属性作为 HTML 片段处理
        center: false //文字是否居中
      };
    },

    computed: {
      typeClass() {
        return this.type && !this.iconClass
          ? `el-message__icon el-icon-${ typeMap[this.type] }`
          : '';
      },
      //控制顶部的距离
      positionStyle() {
        return {
          'top': `${ this.verticalOffset }px`
        };
      }
    },

    watch: {
      closed(newVal) {
        if (newVal) {
          this.visible = false;
        }
      }
    },

    methods: {
      handleAfterLeave() {
        this.$destroy(true);
        this.$el.parentNode.removeChild(this.$el);
      },
      //关闭
      close() {
        this.closed = true;
        if (typeof this.onClose === 'function') {
          this.onClose(this);
        }
      },

      clearTimer() {
        clearTimeout(this.timer);
      },
      //message出现多久之后关闭
      startTimer() {
        if (this.duration > 0) {
          this.timer = setTimeout(() => {
            if (!this.closed) {
              this.close();
            }
          }, this.duration);
        }
      },
    // esc关闭消息
      keydown(e) {
        if (e.keyCode === 27) { 
          if (!this.closed) {
            this.close();
          }
        }
      }
    },
    mounted() {
      this.startTimer();
      document.addEventListener('keydown', this.keydown);
    },
    beforeDestroy() {
      document.removeEventListener('keydown', this.keydown);
    }
  };
</script>

main.js调用部分(这边用到的PopupManager在后续再分析)

import Vue from 'vue';
import Main from './main.vue';
import { PopupManager } from '@/utils/popup';
import { isVNode } from '@/utils/vdom';
let MessageConstructor = Vue.extend(Main);

let instance;
let instances = [];
let seed = 1;
// Message的初始化方法
const Message = function(options) {
  if (Vue.prototype.$isServer) return;
  options = options || {};
  if (typeof options === 'string') {
    options = {
      message: options
    };
  }
   // 外界传递进来的关闭方法
  let userOnClose = options.onClose;
  let id = 'message_' + seed++; // 弹窗ID,每次增加,保证唯一性

  // 注册关闭事件,在main.vue中触发close事件,调用这里的onClos
  options.onClose = function() {
    Message.close(id, userOnClose);
  };
  instance = new MessageConstructor({  // 创建message对象,main.vue中的data,被options覆盖
    data: options
  });
  instance.id = id;
    //isVNode主要是针对 接收一个 VNode 作为参数如下面格式:
    // const h = this.$createElement;
    //     this.$message({
    //       message: h('p', null, [
    //         h('span', null, '内容可以是 '),
    //         h('i', { style: 'color: teal' }, 'VNode')
    //       ])
    //     });
  if (isVNode(instance.message)) {
    // 把这个赋值给默认的slot
    instance.$slots.default = [instance.message];
    instance.message = null;
  }
  // 挂载这个实例 插入到body
  instance.$mount();
  document.body.appendChild(instance.$el); // 在body上添加弹出message
  let verticalOffset = options.offset || 20;
  instances.forEach(item => {
    verticalOffset += item.$el.offsetHeight + 16; //// 根据现在有多少个message弹出计算实际位置
  });
  instance.verticalOffset = verticalOffset; // 给实例赋值据顶端偏移
  instance.visible = true;
  instance.$el.style.zIndex = PopupManager.nextZIndex();
  instances.push(instance);
  return instance;
};
// 为单独调用的方式,绑定初始化事件。如:Message.success(options)
['success', 'warning', 'info', 'error'].forEach(type => {
  Message[type] = options => {
    if (typeof options === 'string') {
      options = {
        message: options
      };
    }
    options.type = type;
    return Message(options);
  };
});
// 弹窗的关闭事件
Message.close = function(id, userOnClose) {
  let len = instances.length;
  let index = -1;
  let removedHeight;
  for (let i = 0; i < len; i++) {
    if (id === instances[i].id) {
      removedHeight = instances[i].$el.offsetHeight;
      index = i;
      if (typeof userOnClose === 'function') {
        userOnClose(instances[i]);
      }
       // 删除这个实例
      instances.splice(i, 1);
      break;
    }
  }
  if (len <= 1 || index === -1 || index > instances.length - 1) return;
  for (let i = index; i < len - 1 ; i++) {
    let dom = instances[i].$el;
    dom.style['top'] =
      parseInt(dom.style['top'], 10) - removedHeight - 16 + 'px';
  }
};
//手动关闭所有实例
Message.closeAll = function() {
  for (let i = instances.length - 1; i >= 0; i--) {
    instances[i].close();
  }
};

export default Message;