elementUI 源码-打造自己的组件库,系列四:Dialog组件(原创)

先上车

废了废了,中间一堆事耽误了迟迟没有更新
来吧继续阅读组件源码,dialog组件安排上
个人觉得dialog组件的难点在于弹出框的流程,特别是层级的处理
当然其他小的知识点也是不少的,这就是阅读源码的快乐

上源码

源码大致分为三部分阅读,先易后难没毛病

html部分

打开packages/dialog/src/component.vue


总结:
1、亮点:通过改变key值来销毁div(铁子们还知道vue中key的其他用法吗?)
2、vue内置transition组件及其钩子函数的灵活运用
3、@click.self 指触发元素自身时生效

script部分

还是packages/dialog/src/component.vue


总结:
1、了解了dialog组件的整体流程,以及dialog各个Attributes参数的实际作用
2、通过dialog组件可以看到elementUI的严谨性,如事件的绑定与移除,元素的插入与销毁,$nextTick合理利用
3、复习了.sync语法糖,$emit('update:visible')的用途

难点部分

emitter.js

打开packages/src/mixins/emitter
先来点开胃菜
如上文所说,emitter.js定义了dispatchbroadcast方法用来派发和广播事件
1)dispatch是用于派发事件到父组件以及更上级别的指定组件进行接收的
2)broadcast方法主要用于将数据或者方法广播到子组件以及子孙指定组件进行接收

/**
 * 广播方法定义
 * @param String componentName 组件名称
 * @param String eventName 事件名称
 * @param Object params 参数
 */

function broadcast(componentName, eventName, params) {
  // 遍历子组件,对子组件的componentName进行匹配
  // 阅读源码,发现很多组件除了定义name属性外,还定义了componentName了,这里就了解componentName的作用
  this.$children.forEach(child => {
    var name = child.$options.componentName;
    if (name === componentName) {
      // 子组件中与传入的componentName相等时,则在子组件中执行eventName方法,参数为params
      // 注意$emit会触发组件中的$on事件(vue中内置的$emit、$on)
      // 通过apply将this指向为当前组件,apply第二个参数为一个数组
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      // 如果不存在则继续执行broadcast方法,this指向子组件
      broadcast.apply(child, [componentName, eventName].concat([params]));
    }
  });
}
export default {
  methods: {
    /**
     * 派发方法定义
     * @param String componentName 组件名称
     * @param String eventName 事件名称
     * @param Object params 参数
     */
    dispatch(componentName, eventName, params) {
      // 通过while循环找到对应的父组件(找父组件的场景在平常开发中也会用到)
      // 定义父组件对象,如果该组件上面没有对象,则parent为根组件
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      // 当父组件对象存在时且父组件名称不等于componentName时,则改变parent值,并将parent值向上赋值;当parent不存在或者name === componentName时,跳出循环
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;
        // 如果父组件存在,取父组件的componentName
        if (parent) {
          name = parent.$options.componentName;
        }
      }
      // 找到对应的父组件时,执行该组件中eventName方法,参数为params
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};

popup

打开packages/src/utils/popup/index.js
重点来了,来吧一起了解下dialog弹框的流程
dialog弹框打开的流程总结:
1、通过mixins混入popup/index.js
2、watch监听visible属性的变化,为true时先执行this.open方法,再执行this.doOpen方法
3、在this.doOpen方法中调用PopupManager.openModal打开遮罩
4、lockScroll属性为true时,给body设置overflow: hidden;实现弹框打开时将body滚动锁定
5、给当前的dialog组件加上层级,层级的高度比遮罩高一层
6、如果打开多个弹框,公用一个遮罩,通过openModal控制遮罩的层级
(下面会分析PopupManager.openModal方法)

dialog弹框关闭的流程总结:
1、watch监听visible属性的变化,为false时先执行this.close方法,再执行this.doClose方法
2、在this.doAfterClose方法中调用PopupManager.closeModal关闭遮罩
3、关闭遮罩时如果存在多个弹框,需将遮罩的层级降为上一个弹框的层级
(下面会分析PopupManager.closeModal方法)

import Vue from 'vue';
import merge from 'element-ui/src/utils/merge';
import PopupManager from 'element-ui/src/utils/popup/popup-manager';
import getScrollBarWidth from '../scrollbar-width';
import { getStyle, addClass, removeClass, hasClass } from '../dom';

let idSeed = 1;
let scrollBarWidth;
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    openDelay: {},
    closeDelay: {},
    zIndex: {},
    modal: {
      type: Boolean,
      default: false
    },
    modalFade: {
      type: Boolean,
      default: true
    },
    modalClass: {},
    modalAppendToBody: {
      type: Boolean,
      default: false
    },
    lockScroll: {
      type: Boolean,
      default: true
    },
    closeOnPressEscape: {
      type: Boolean,
      default: false
    },
    closeOnClickModal: {
      type: Boolean,
      default: false
    }
  },

  beforeMount() {
    // 生成一个_popupId,调用PopupManager.register将当前组件的实例对象注册到instances中
    this._popupId = 'popup-' + idSeed++;
    PopupManager.register(this._popupId, this);
  },

  // 关闭时 销毁对应的实例,并移除body的class类名
  beforeDestroy() {
    PopupManager.deregister(this._popupId);
    PopupManager.closeModal(this._popupId);

    this.restoreBodyStyle();
  },

  data() {
    return {
      opened: false,
      bodyPaddingRight: null,
      computedBodyPaddingRight: 0,
      withoutHiddenClass: true,
      rendered: false
    };
  },

  watch: {
    visible(val) {
      // 同dialog组件一样,也是监听visible
      if (val) {
        if (this._opening) return;
        if (!this.rendered) {
          this.rendered = true;
          Vue.nextTick(() => {
            // 第一次进入到这里
            this.open();
          });
        } else {
          this.open();
        }
      } else {
        // 进入关闭的流程
        this.close();
      }
    }
  },

  methods: {
    open(options) {
      if (!this.rendered) {
        this.rendered = true;
      }
      // 通过merge方法 合并props,这里没用到
      const props = merge({}, this.$props || this, options);

      if (this._closeTimer) {
        clearTimeout(this._closeTimer);
        this._closeTimer = null;
      }
      clearTimeout(this._openTimer);

      const openDelay = Number(props.openDelay);
      if (openDelay > 0) {
        this._openTimer = setTimeout(() => {
          this._openTimer = null;
          this.doOpen(props);
        }, openDelay);
      } else {
        // 到这里  我们来看
        this.doOpen(props);
      }
    },

    doOpen(props) {
      // 是否是服务端渲染
      if (this.$isServer) return;
      if (this.willOpen && !this.willOpen()) return;
      if (this.opened) return;
      // 这里 _opening = true
      this._opening = true;
      // 这个 this.$el 就是dialog的el-dialog__wrapper元素
      const dom = this.$el;

      // modal属性 是否需要遮罩层
      const modal = props.modal;

      const zIndex = props.zIndex;
      // 第一次props.zIndex为undefined
      if (zIndex) {
        PopupManager.zIndex = zIndex;
      }

      // 有遮罩层
      if (modal) {
        // 如果正在关闭  现在基本跟我们没有 关系
        if (this._closing) {
          PopupManager.closeModal(this._popupId);
          this._closing = false;
        }
        /**
         * PopupManager.openModal是用来控制灰色遮罩的打开
         * @param String _popupId 弹窗的id
         * @param String PopupManager.nextZIndex 弹窗的zIndex层级
         * @param Object this.modalAppendToBody ? undefined : dom 如果设置modal-append-to-body属性,传入undefined,否则传入当前组件
         * @param String modalClass modal弹层的显示时候的 class
         * @param Boolean modalFade 是否是淡入淡出
         */
        PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);
        // 如果设置了lock-scroll属性,默认为true
        if (props.lockScroll) {
          // 这边的话 判断body是不是有 el-popup-parent--hidden
          this.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden');
          if (this.withoutHiddenClass) {
            // 获取到 body的 padding-right
            this.bodyPaddingRight = document.body.style.paddingRight;
            this.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10);
          }
          // getScrollBarWidth方法用来获取浏览器默认的滚动条宽度
          scrollBarWidth = getScrollBarWidth();
          // 判断body是否需要滚动
          let bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight;
          // 查看body overflowY 属性
          let bodyOverflowY = getStyle(document.body, 'overflowY');
          // 总的来说这边条件就是说 body边上 有滚动条了 那么就给body加上 相应的 padding-right
          // 免得 body 设置上 overflow 为 hidden的时候滚动条消失 页面变宽  发生页面的抖动
          if (scrollBarWidth > 0 && (bodyHasOverflow || bodyOverflowY === 'scroll') && this.withoutHiddenClass) {
            document.body.style.paddingRight = this.computedBodyPaddingRight + scrollBarWidth + 'px';
          }
          // 给body添加el-popup-parent--hidden类名,该类名的样式为overflow: hidden; 从而实现将 body 滚动锁定
          addClass(document.body, 'el-popup-parent--hidden');
        }
      }

      // 如果dialog外层是没有定位的话  那么就加上 absolute
      if (getComputedStyle(dom).position === 'static') {
        dom.style.position = 'absolute';
      }
      // 给当前的dialog组件加上层级,层级的高度比遮罩高一层
      dom.style.zIndex = PopupManager.nextZIndex();
      this.opened = true;

      this.onOpen && this.onOpen();

      this.doAfterOpen();
    },

    doAfterOpen() {
      // _opening正在打开属性设为false, 打开弹框的流程就是这样了
      this._opening = false;
    },

    close() {
      if (this.willClose && !this.willClose()) return;

      if (this._openTimer !== null) {
        clearTimeout(this._openTimer);
        this._openTimer = null;
      }
      clearTimeout(this._closeTimer);

      const closeDelay = Number(this.closeDelay);

      if (closeDelay > 0) {
        this._closeTimer = setTimeout(() => {
          this._closeTimer = null;
          this.doClose();
        }, closeDelay);
      } else {
        this.doClose();
      }
    },

    doClose() {
      // _closing 正在关闭的属性设为true
      this._closing = true;

      this.onClose && this.onClose();

      if (this.lockScroll) {
        setTimeout(this.restoreBodyStyle, 200);
      }

      this.opened = false;

      this.doAfterClose();
    },

    doAfterClose() {
      /**
       *  PopupManager.closeModal 用来控制灰色遮罩的关闭
       * @param String _popupId 弹窗的id
       */
      PopupManager.closeModal(this._popupId);
      // _closing 正在关闭的属性设为false, 关闭流程介绍
      this._closing = false;
    },

    restoreBodyStyle() {
      if (this.modal && this.withoutHiddenClass) {
        document.body.style.paddingRight = this.bodyPaddingRight;
        removeClass(document.body, 'el-popup-parent--hidden');
      }
      this.withoutHiddenClass = true;
    }
  }
};

export {
  PopupManager
};

打开packages/src/utils/popup/popup-manager.js
重点分析下PopupManager.openModalPopupManager.closeModal方法

import Vue from 'vue';
import { addClass, removeClass } from 'element-ui/src/utils/dom';

let hasModal = false;
let hasInitZIndex = false;
let zIndex;

// getModal方法用来生成弹框的灰色遮罩
// 遮罩只用生成一次,然后存到PopupManager.modalDom中,所有的弹框都用同一个遮罩
const getModal = function() {
  if (Vue.prototype.$isServer) return;
  let modalDom = PopupManager.modalDom;
  if (modalDom) {
    hasModal = true;
  } else {
    hasModal = false;
    modalDom = document.createElement('div');
    PopupManager.modalDom = modalDom;

    // 给遮罩绑定上touchmove事件
    modalDom.addEventListener('touchmove', function(event) {
      event.preventDefault();
      event.stopPropagation();
    });

    // 给遮罩绑定上click事件
    modalDom.addEventListener('click', function() {
      PopupManager.doOnModalClick && PopupManager.doOnModalClick();
    });
  }

  return modalDom;
};

const instances = {};

const PopupManager = {
  // 是否是淡入淡出
  modalFade: true,

  // 获取instance上的实例
  getInstance: function(id) {
    return instances[id];
  },

  // 往instance上的注册实例
  register: function(id, instance) {
    if (id && instance) {
      instances[id] = instance;
    }
  },

  // instance上的销毁实例
  deregister: function(id) {
    if (id) {
      instances[id] = null;
      delete instances[id];
    }
  },

  // 计算zIndex层级
  nextZIndex: function() {
    return PopupManager.zIndex++;
  },

  // 存储弹框的栈
  modalStack: [],

  // 执行对应弹框组件上的close方法
  doOnModalClick: function() {
    const topItem = PopupManager.modalStack[PopupManager.modalStack.length - 1];
    if (!topItem) return;

    const instance = PopupManager.getInstance(topItem.id);
    if (instance && instance.closeOnClickModal) {
      instance.close();
    }
  },

  // 该方法用来打开弹框的灰色遮罩
  openModal: function(id, zIndex, dom, modalClass, modalFade) {
    if (Vue.prototype.$isServer) return;
    if (!id || zIndex === undefined) return;
    // 这里要注意this指向,此时的this为PopupManager对象
    this.modalFade = modalFade;

    // 第一次这个栈 默认是个空的数组[]
    const modalStack = this.modalStack;

    // 遍历栈,找到对应id的弹框
    for (let i = 0, j = modalStack.length; i < j; i++) {
      const item = modalStack[i];
      if (item.id === id) {
        return;
      }
    }

    // 获取灰色遮罩的dom元素
    const modalDom = getModal();
    /* 给遮罩加上 v-modal类名,遮罩的半透明背景就是这样类名设置的
      .v-modal {
      position: fixed;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      opacity: 0.5;
      background: #000000;
    }*/
    addClass(modalDom, 'v-modal');
    if (this.modalFade && !hasModal) {
      // 加上v-modal-enter渐变的类名
      addClass(modalDom, 'v-modal-enter');
    }
    if (modalClass) {
      let classArr = modalClass.trim().split(/\s+/);
      classArr.forEach(item => addClass(modalDom, item));
    }
    // 200毫秒后去掉 v-modal-enter
    setTimeout(() => {
      removeClass(modalDom, 'v-modal-enter');
    }, 200);

    // 将遮罩添加到body上
    if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
      dom.parentNode.appendChild(modalDom);
    } else {
      document.body.appendChild(modalDom);
    }

    if (zIndex) {
      modalDom.style.zIndex = zIndex;
    }
    modalDom.tabIndex = 0;
    modalDom.style.display = '';

    // 将当前弹框的id 已经遮罩的zIndex 存到modalStack中
    this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });
  },

  // 用来关闭遮罩(存在同时打开多个弹框的情况)
  /*
  * 1、根据id 获取对应的遮罩对象
  * 2、如果这个id就是modalStack最后一个,直接pop,并将遮罩的层级降为此时modalStack最后一个的层级
  * 3、如果modalStack.length === 0,也就是此时页面没有弹框了,将body上的modalDom移除,并PopupManager.modalDom = undefined
  * */
  closeModal: function(id) {
    // 获取modalStack
    const modalStack = this.modalStack;
    const modalDom = getModal();

    if (modalStack.length > 0) {
      // 取出最后一个
      const topItem = modalStack[modalStack.length - 1];
      if (topItem.id === id) {
        // 如果有当前的这个有modalClass  那么把这些个class都去掉
        if (topItem.modalClass) {
          let classArr = topItem.modalClass.trim().split(/\s+/);
          classArr.forEach(item => removeClass(modalDom, item));
        }
        // 最后一个删除掉
        modalStack.pop();
        // 还有的话,也就是同时打开多个弹框的情况
        if (modalStack.length > 0) {
          // 将遮罩的层级降为此时modalStack最后一个的层级
          // 一般来说就是  流程是
          // modal 层级 2001  对话框层级 2002
          // 在打开一个对话框 modal层级2003 对话框层级2004
          // 关闭一个对话框  modal 层级变为又要变成2001放在 层级在第一个对话框的下面
          modalDom.style.zIndex = modalStack[modalStack.length - 1].zIndex;
        }
      } else {
        // 如果要移除的不是最后一个 那么只要将这个对象移除就行了 层级不用做什么操作
        for (let i = modalStack.length - 1; i >= 0; i--) {
          if (modalStack[i].id === id) {
            modalStack.splice(i, 1);
            break;
          }
        }
      }
    }

    // 所有弹框都关闭的情况
    if (modalStack.length === 0) {
      // 加入淡入淡出的样式
      if (this.modalFade) {
        addClass(modalDom, 'v-modal-leave');
      }
      setTimeout(() => {
        if (modalStack.length === 0) {
          // 从body上移除遮罩,并重置PopupManager.modalDom
          if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);
          modalDom.style.display = 'none';
          PopupManager.modalDom = undefined;
        }
        removeClass(modalDom, 'v-modal-leave');
      }, 200);
    }
  }
};

// 通过Object.defineProperty对PopupManager上的zIndex的拦截
// 第一次获取zIndex时,返回初始值为2000
Object.defineProperty(PopupManager, 'zIndex', {
  configurable: true,
  get() {
    if (!hasInitZIndex) {
      zIndex = zIndex || (Vue.prototype.$ELEMENT || {}).zIndex || 2000;
      hasInitZIndex = true;
    }
    return zIndex;
  },
  set(value) {
    zIndex = value;
  }
});

const getTopPopup = function() {
  if (Vue.prototype.$isServer) return;
  if (PopupManager.modalStack.length > 0) {
    const topPopup = PopupManager.modalStack[PopupManager.modalStack.length - 1];
    if (!topPopup) return;
    const instance = PopupManager.getInstance(topPopup.id);

    return instance;
  }
};

if (!Vue.prototype.$isServer) {
  // handle `esc` key when the popup is shown
  window.addEventListener('keydown', function(event) {
    if (event.keyCode === 27) {
      const topPopup = getTopPopup();

      if (topPopup && topPopup.closeOnPressEscape) {
        topPopup.handleClose
          ? topPopup.handleClose()
          : (topPopup.handleAction ? topPopup.handleAction('cancel') : topPopup.close());
      }
    }
  });
}

export default PopupManager;

到站

1、总体来说,通过阅读dialog组件源码,感觉还是挺惊艳的。没想到一个小小的弹框组件,也是内有乾坤啊。
2、弹框的流程控制、层级控制、组件之间的派发与广播、如何递归向上查找父组件、对.sync的运用、甚至是$emit、$on,这些都可以运用到平常的开发中。

参考链接

vue elementUi dialog组件 逐行解读分析
element-ui的dispatch和broadcast方法分析

你可能感兴趣的:(前端组件库,elementui,vue.js,javascript,ui,前端)