vue elementUi dialog组件 逐行解读分析

1,395 阅读5分钟

dialog组件分析

示例代码

<template>
  <!-- 示例代码 -->
  <div>
    <el-button type="text" @click="outerVisible = true">点击打开外层 Dialog</el-button>
    <el-dialog title="外层 Dialog" :visible.sync="outerVisible">
      <el-dialog
        width="30%"
        title="内层 Dialog"
        :visible.sync="innerVisible"
        append-to-body>
      </el-dialog>
      <div slot="footer" class="dialog-footer">
        <el-button @click="outerVisible = false">取 消</el-button>
        <el-button type="primary" @click="innerVisible = true">打开内层 Dialog</el-button>
      </div>
    </el-dialog>
  </div>
</template>
  • 看下两层对话框是怎么处理的
  • 首先来看下对话框初始化显示的处理 逻辑

dialog组件代码

<template>
  <transition
    name="dialog-fade"
    @after-enter="afterEnter"
    @after-leave="afterLeave">
    <div
      v-show="visible"
      class="el-dialog__wrapper"
      @click.self="handleWrapperClick">
      <div
        role="dialog"
        :key="key"
        aria-modal="true"
        :aria-label="title || 'dialog'"
        :class="['el-dialog', { 'is-fullscreen': fullscreen, 'el-dialog--center': center }, customClass]"
        ref="dialog"
        :style="style">
        <div class="el-dialog__header">
          <slot name="title">
            <span class="el-dialog__title">{{ title }}</span>
          </slot>
          <button
            type="button"
            class="el-dialog__headerbtn"
            aria-label="Close"
            v-if="showClose"
            @click="handleClose">
            <i class="el-dialog__close el-icon el-icon-close"></i>
          </button>
        </div>
        <div class="el-dialog__body" v-if="rendered"><slot></slot></div>
        <div class="el-dialog__footer" v-if="$slots.footer">
          <slot name="footer"></slot>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
import Popup from '@/utils/popup';
import Migrating from '@/mixins/migrating';
import emitter from '@/mixins/emitter';

export default {
  name: 'ElDialog',

  mixins: [Popup, emitter, Migrating],

  props: {
    title: {
      type: String,
      default: '',
    },

    modal: {
      type: Boolean,
      default: true,
    },

    modalAppendToBody: {
      type: Boolean,
      default: true,
    },

    appendToBody: {
      type: Boolean,
      default: false,
    },

    lockScroll: {
      type: Boolean,
      default: true,
    },

    closeOnClickModal: {
      type: Boolean,
      default: true,
    },

    closeOnPressEscape: {
      type: Boolean,
      default: true,
    },

    showClose: {
      type: Boolean,
      default: true,
    },

    width: String,

    fullscreen: Boolean,

    customClass: {
      type: String,
      default: '',
    },

    top: {
      type: String,
      default: '15vh',
    },
    beforeClose: Function,
    center: {
      type: Boolean,
      default: false,
    },

    destroyOnClose: Boolean,
  },

  data() {
    return {
      closed: false,
      key: 0,
    };
  },

  watch: {
    visible(val) {
      if (val) {
        this.closed = false;
        this.$emit('open');
        this.$el.addEventListener('scroll', this.updatePopper);
        this.$nextTick(() => {
          this.$refs.dialog.scrollTop = 0;
        });
        if (this.appendToBody) {
          document.body.appendChild(this.$el);
        }
      } else {
        this.$el.removeEventListener('scroll', this.updatePopper);
        if (!this.closed) this.$emit('close');
        if (this.destroyOnClose) {
          this.$nextTick(() => {
            this.key++;
          });
        }
      }
    },
  },

  computed: {
    style() {
      const style = {};
      if (!this.fullscreen) {
        style.marginTop = this.top;
        if (this.width) {
          style.width = this.width;
        }
      }
      return style;
    },
  },

  methods: {
    getMigratingConfig() {
      return {
        props: {
          size: 'size is removed.',
        },
      };
    },
    handleWrapperClick() {
      if (!this.closeOnClickModal) return;
      this.handleClose();
    },
    handleClose() {
      if (typeof this.beforeClose === 'function') {
        this.beforeClose(this.hide);
      } else {
        this.hide();
      }
    },
    hide(cancel) {
      if (cancel !== false) {
        this.$emit('update:visible', false);
        this.$emit('close');
        this.closed = true;
      }
    },
    updatePopper() {
      this.broadcast('ElSelectDropdown', 'updatePopper');
      this.broadcast('ElDropdownMenu', 'updatePopper');
    },
    afterEnter() {
      this.$emit('opened');
    },
    afterLeave() {
      this.$emit('closed');
    },
  },

  mounted() {
    if (this.visible) {
      this.rendered = true;
      this.open();
      if (this.appendToBody) {
        document.body.appendChild(this.$el);
      }
    }
  },

  destroyed() {
    // if appendToBody is true, remove DOM node after destroy
    if (this.appendToBody && this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el);
    }
  },
};
</script>

  • 我们来看下这个初始化吧
// 可以看到一开始我们是隐藏的那这个就不用看了
mounted() {
    if (this.visible) {
      this.rendered = true;
      this.open();
      if (this.appendToBody) {
        document.body.appendChild(this.$el);
      }
    }
  },
  • 在来看下这边有引入一个mixins Popup 看下这个初始化有没做什么
// 初始化了 这个_popupId  然后往PopupManager 注册了一个东西
beforeMount() {
  this._popupId = `popup-${idSeed++}`;
  PopupManager.register(this._popupId, this);
},
// 来看下 PopupManager.register
// instances是一个内部变量存储对象  把这个dialog组件 存起来
register(id, instance) {
  if (id && instance) {
    instances[id] = instance;
  }
},
  • OK 那我们继续
  • 那么首先要显示第一步肯定就是 visible=true
  • 我们来看下发生肾么事了 本身组件这边有个监听
  watch: {
    visible(val) {
      if (val) {
        // 那么肯定就是走这边逻辑了
        this.closed = false;
        // OK 这边发射这个 open 事件 
        this.$emit('open');
        // 这个监听浏览器的滚动  然后就处罚这个函数
        // updatePopper() {
        //   this.broadcast('ElSelectDropdown', 'updatePopper');
        //   this.broadcast('ElDropdownMenu', 'updatePopper');
        // },
        // 我们可以看到 broadcast 向下级组件广播 让他们更新 Popper
        // 这个主要是为了 让dialog里面有包含 select 等组件 是打开状态的时候需要更新位置
        // 这个下面分析到 select 再说
        this.$el.addEventListener('scroll', this.updatePopper);
        // 然后dialog 滚动到顶部
        this.$nextTick(() => {
          this.$refs.dialog.scrollTop = 0;
        });
        // 这个默认是false   那就显示在原来的位置咯
        // 那么到这里 dialog 就显出出来了   
        // 但是 这边什么 遮罩层啥的呢  是怎么出来的
        // OK  我们别忘了   这边mixins的 Popup   我们来看下做了什么
        if (this.appendToBody) {
          document.body.appendChild(this.$el);
        }
      } else {
        this.$el.removeEventListener('scroll', this.updatePopper);
        if (!this.closed) this.$emit('close');
        if (this.destroyOnClose) {
          this.$nextTick(() => {
            this.key++;
          });
        }
      }
    },
  },

Popup

我们看这边也监听了 visible

  watch: {
    visible(val) {
      // OK 这边是显示的 那么我进来
      if (val) {
        // 那么第一次 这个是false  继续
        if (this._opening) return;
        // 第一次也是false  继续进去
        if (!this.rendered) {
          this.rendered = true;
          // 然后等下次事件环 调用this.open  我们来看下
          Vue.nextTick(() => {
            this.open();
          });
        } else {
          this.open();
        }
      } else {
        this.close();
      }
    },
  },
  • this.open
open(options) {
  if (!this.rendered) {
    this.rendered = true;
  }
  // 那么 来到这边  合并下options 这里没啥
  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);
  }
},
  • this.doOpen(props)
doOpen(props) {
  // 是否是服务端渲染
  if (this.$isServer) return;
  // 那这边也没有管我们的事 继续
  if (this.willOpen && !this.willOpen()) return;
  if (this.opened) return;
  // OK  这伙 标识符设置上 _opening = true
  this._opening = true;
  // 这个 this.$el 就是dialog的最外层元素了 
  const dom = this.$el;
  // 这个就是 遮罩层了 dialog默认是 true
  const { modal } = props;
  // 看看有没有传 zIndex  基本是没有传了
  const { zIndex } = props;
  if (zIndex) {
    PopupManager.zIndex = zIndex;
  }
  // 有遮罩层
  if (modal) {
    // 如果正在关闭  现在基本跟我们没有 关系
    if (this._closing) {
      PopupManager.closeModal(this._popupId);
      this._closing = false;
    }
    // OK 这边我们 看下 PopupManager这个类  打开openModal 
    // 传入的有这个 dialog的 this._popupId  
    // 下个层级  PopupManager.nextZIndex()
    // nextZIndex() {
    //   return PopupManager.zIndex++;
    // },
    // 第三个 默认dialog是 true
    // 第四个第五个 是传的 modal弹层的显示时候的 class  然后是否是淡入淡出
    // 继续看 openModal
    PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);
    // 现在模态框是现实出来了
    // 是否让下面的是锁定的 默认是的
    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);
      }
      // 获取到 滚动条的 宽度
      scrollBarWidth = getScrollBarWidth();
      // 当前body是否是 超出的状态   也就是说有没有滚动条了
      const bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight;
      // 查看body  overflowY 属性
      const 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`;
      }
      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;
  // 所以就是打开来了  有监听onOpen的执行
  this.onOpen && this.onOpen();

  this.doAfterOpen();
  // 那么初始化打开就完全了哦
  // 接下来在看下关闭
},
// doAfterOpen
doAfterOpen() {
  // 设置这个标识为 false
  this._opening = false;
},

  • PopupManager.openModal
openModal(id, zIndex, dom, modalClass, modalFade) {
  if (Vue.prototype.$isServer) return;
  if (!id || zIndex === undefined) return;
  this.modalFade = modalFade;
  // 这个栈 默认是个空的数组[]   当前的this指得是PopupManager这个类了
  const { modalStack } = this;
  // 找下看看有没有这个modal  第一次 当然是没有咯
  for (let i = 0, j = modalStack.length; i < j; i++) {
    const item = modalStack[i];
    if (item.id === id) {
      return;
    }
  }
  // 然后这边就是 getModal()  我们在来看 
  const modalDom = getModal();
  // 然后给这个模态框加这个样式   然后是淡入淡出那就在加个样式
  addClass(modalDom, 'v-modal');
  if (this.modalFade && !hasModal) {
    addClass(modalDom, 'v-modal-enter');
  }
  // 有自己传的样式加上
  if (modalClass) {
    const 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 层级 class
  // 然后我们在回头看  doOpen
  this.modalStack.push({ id, zIndex, modalClass });
},

  • getModal
const getModal = function () {
  if (Vue.prototype.$isServer) return;
  // 解析出 modalDom 那么目前肯定是 undefined咯
  let { modalDom } = PopupManager;
  if (modalDom) {
    hasModal = true;
  } else {
    // 来到这里
    hasModal = false;
    // 创建一个div 给这个赋值上
    modalDom = document.createElement('div');
    PopupManager.modalDom = modalDom;
    // 模态框屏蔽这个 touchmove
    modalDom.addEventListener('touchmove', (event) => {
      event.preventDefault();
      event.stopPropagation();
    });
    // 模态框监听 click 时间    这个一般就是作用就是点击模态框的时候关闭 对话框
    modalDom.addEventListener('click', () => {
      PopupManager.doOnModalClick && PopupManager.doOnModalClick();
    });
  }
  // 返回这个 modalDom 
  // 接下来往回去看 openModal
  return modalDom;
};

朋友们按照我们的示例 第二次打开里面一层的对话框 那么当前就是有两个了

  • 我们来看下流程 有什么不同
  • 对话框组件那边是 没啥不同了
  • 我们来看下 Popup 这边的 modal框逻辑来看下 因为两个对话框的话 饿了么 组件设计其实是只有一个 modal 遮罩层的
  • 我们来看下处理
  • 进入到 PopupManager.openModal(...)
  openModal(id, zIndex, dom, modalClass, modalFade) {
    if (Vue.prototype.$isServer) return;
    if (!id || zIndex === undefined) return;
    this.modalFade = modalFade;

    const { modalStack } = this;

    for (let i = 0, j = modalStack.length; i < j; i++) {
      const item = modalStack[i];
      if (item.id === id) {
        return;
      }
    }
    // 来看这里
    const modalDom = getModal();
    // 然后对刚刚的  遮罩层  加这个 不过已经有了
    addClass(modalDom, 'v-modal');
    // 然后加个动画   这个就是刚刚为啥 200毫秒要删掉了
    if (this.modalFade && !hasModal) {
      addClass(modalDom, 'v-modal-enter');
    }
    // 
    if (modalClass) {
      const classArr = modalClass.trim().split(/\s+/);
      classArr.forEach((item) => addClass(modalDom, item));
    }
    setTimeout(() => {
      removeClass(modalDom, 'v-modal-enter');
    }, 200);
    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 = '';
    // 继续把当前这个模态框的状态加入到这个栈中 
    this.modalStack.push({ id, zIndex, modalClass });
    // 然后就是 继续 看doOpen  跟刚刚的一样了
  },
const getModal = function () {
  if (Vue.prototype.$isServer) return;
  let { modalDom } = PopupManager;
  // 因为我们刚刚已经赋值了 所以已经有了  所以直接返回了 刚刚那个遮罩层
  if (modalDom) {
    hasModal = true;
  } else {
    // ...
  }
  return modalDom;
};

感觉今天这个写的有所进步啊

今天先到这边 明天看下 把close 关闭的流程梳理下 。。。

更新下

继续搞起来

关闭那么 visible = false

// dialog  component 这边的处理
visible(val) {
  if (val) {
    // ...
  } else {
    // 移除这个事件  因为现实的时候有 监听
    this.$el.removeEventListener('scroll', this.updatePopper);
    if (!this.closed) this.$emit('close');
    // prop 一个属性  关闭时是否销毁Dialog中的元素
    if (this.destroyOnClose) {
      // 因为模板中有个 <div role="dialog" :key="key"></div>  所以改变key值 就会让vue销毁里面的元素
      // OK  那么这边及解读完了   接下来来看下 mixins中的 Popup 
      this.$nextTick(() => {
        this.key++;
      });
    }
  }
},
  • Popup 下面的watch
visible(val) {
  if (val) {
    // ...
  } else {
    // Ok 直接走这边来
    this.close();
  }
},
close() {
  // OK 这些没有继续
  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
doClose() {
  // 设置这个标识符  正在关闭
  this._closing = true;
  // 这边也没有
  this.onClose && this.onClose();
  // 是否是锁定的 是的话就恢复body原来的样式  
  // restoreBodyStyle() {
  //     if (this.modal && this.withoutHiddenClass) {
  //       document.body.style.paddingRight = this.bodyPaddingRight;
  //       removeClass(document.body, 'el-popup-parent--hidden');
  //     }
  //     this.withoutHiddenClass = true;
  //  },
  if (this.lockScroll) {
    setTimeout(this.restoreBodyStyle, 200);
  }
  // 标识符设置
  this.opened = false;
  // 在来看这个方法
  this.doAfterClose();
},
  • popup -> doAfterClose
doAfterClose() {
  // 调用这个  这个要小小的看下  传入当前的ID
  PopupManager.closeModal(this._popupId);
  this._closing = false;
},
closeModal(id) {
  // 取出这个栈  因为刚刚我们是打开了 两个不同的 dialog 所以现在这个栈里面有两个对象
  // 我们假设是  [{id: 1, zIndex: 2000, modalClass: {}}, {id: 2, zIndex: 2002, modalClass: {}}]
  const { modalStack } = this;
  const modalDom = getModal();
  // OK  我们有两个  那么大于0 进去
  if (modalStack.length > 0) {
    // 取出最后一个
    const topItem = modalStack[modalStack.length - 1];
    // 最后一个 跟当前要关闭的 modal 是一个
    if (topItem.id === id) {
      // 如果有当前的这个有modalClass  那么把这些个class都去掉
      if (topItem.modalClass) {
        const classArr = topItem.modalClass.trim().split(/\s+/);
        classArr.forEach((item) => removeClass(modalDom, item));
      }
      // 最后一个删除掉
      modalStack.pop();
      // 删除掉还有一个的话
      if (modalStack.length > 0) {
        // 那么我们就设置这个的 层级为   现在的最后一个   
        // 也就是id是2的没了   那么就设置成id为1这个对象的层级   
        // 一般来说就是  流程是
        // 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) {
    // 我感觉如果是 没有栈了  那就是没有modal啊  这边感觉也没啥意义吧
    if (this.modalFade) {
      addClass(modalDom, 'v-modal-leave');
    }
    setTimeout(() => {
      if (modalStack.length === 0) {
        // 元素给删掉 然后复原下咯  那就差不多了啊 基本也没了
        if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);
        modalDom.style.display = 'none';
        PopupManager.modalDom = undefined;
      }
      removeClass(modalDom, 'v-modal-leave');
    }, 200);
  }
},

这边也基本是分析完了

大家一起学习探讨