el-drawer组件源码学习

el-Drawer

种一棵🌲最好的时间是十年前,其次是现在。

背景

在优化公司自定义抽屉组件的过程中,我遇到了一些关于抽屉组件功能实现的疑问。为了解决这些疑问,我开始了element-ui库中el-drawer组件源码的阅读,以下就是新手小白的浅薄理解。

主要困惑点

  1. 遮罩层实现机制 :如何通过popup混入实现遮罩层。

  2. 外部点击关闭功能:组件外点击如何触发组件关闭。

关键实现

1. 遮罩层

el-drawer组件通过混入popup外部文件来实现其功能。关键步骤包括:

  1. 初始化 : 在组件挂载到DOM之前,生成唯一的_popupId,并使用唯一的id注册一个新的popup实例。
js 复制代码
//element/src/utils/popup/index.<p align=left>js</p>
let idSeed = 1;
export default {
    // // Registers the modal component with PopupManager when it's about to be mounted.
	beforeMount() {
    	this._popupId = 'popup-' + idSeed++;
        // Registers a new popup instance with a unique ID.
    	PopupManager.register(this._popupId, this); 
  	},
}
  1. 显示逻辑:监听visible属性,控制遮罩层显示与隐藏。

我们可以看到在popup/index.js中监听了visible属性,visible是控制Drawer组件显示或隐藏的关键变量。当抽屉打开时,会执行open()方法。进而调用了doOpen

js 复制代码
//element/src/utils/popup/index.js
watch: {
    visible(val) {
      if (val) {
        if (this._opening) return;
        if (!this.rendered) {
          this.rendered = true;
          Vue.nextTick(() => {
            this.open();
          });
        } else {
          this.open();
        }
      } else {
        this.close();
      }
    }
  },
  1. 核心实现doOpen方法中,根据modal属性决定是否显示遮罩层,并设置相应的zIndex

doOpen():是遮罩层实现的核心,它负责显示遮罩层。

js 复制代码
//element/src/utils/popup/index.js
doOpen(props) {
      if (this.$isServer) return // 方法检查当前环境是否为服务器端渲染,如果是,则直接返回并不执行后续操作
      if (this.willOpen && !this.willOpen()) return // 检查 willOpen 钩子函数是否存在并且返回 false,如果存在并且返回 false,则不执行打开操作
      if (this.opened) return // 检查遮罩层是否已经被打开,如果已经打开,则直接返回。

      this._opening = true // 如果上述检查都通过,将 _opening 属性设置为 true,表示遮罩层正在打开。
      const dom = this.$el; // 获取与遮罩层相关的 DOM 元素($el
      const modal = props.modal; // 传入的是否显示遮罩层属性值
      const zIndex = props.zIndex; //是否自定义了组件的zIndex值
      if (zIndex) {
        PopupManager.zIndex = zIndex;
      }
 		// 显示遮罩层
      if (modal) { 
          // 如果遮罩层正在关闭,则先关闭它
		 if (this._closing) {
          PopupManager.closeModal(this._popupId)
          this._closing = false
        }
         //使用 PopupManager 打开遮罩层,并设置其 zIndex 值。  modalAppendToBody: 默认为false
        PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);
        // 🔔 新手可以跳过这一段if (props.lockScroll) 代码块中的逻辑
       	// 如果设置了 lockScroll 属性,则处理滚动条的锁定。这可能包括在文档体(document.body)上添加一个类来隐藏滚动条,并调整文档体的 paddingRight 以防止内容溢出。
         if (props.lockScroll) {
              this.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden')
              if (this.withoutHiddenClass) {
                this.bodyPaddingRight = document.body.style.paddingRight
                this.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10)
              }
          // scrollBarWidth = getScrollBarWidth()
          const bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight
          const bodyOverflowY = getStyle(document.body, 'overflowY')
          if (scrollBarWidth > 0 && (bodyHasOverflow || bodyOverflowY === 'scroll') && this.withoutHiddenClass) {
            document.body.style.paddingRight = this.computedBodyPaddingRight + scrollBarWidth + 'px'
          }
          addClass(document.body, 'el-popup-parent--hidden')
        }
      }
     // 处理 position 和 zIndex
     // 如果遮罩层的 DOM 元素的 position 样式属性为 static,则将其更改为 absolute,以便能够正确地进行定位和堆叠。
     if (getComputedStyle(dom).position === 'static') {
        dom.style.position = 'absolute'
     }
      dom.style.zIndex = PopupManager.nextZIndex() // 组件的层级要大于遮罩层的层级. 设置 DOM 元素的 zIndex 值,以确保它在其他元素之上显示。
      this.opened = true // 将 opened 属性设置为 true,表示遮罩层已经打开
      this.onOpen && this.onOpen() // 如果定义了 onOpen 方法,则调用它。这允许我们在遮罩层打开时执行自定义逻辑

      this.doAfterOpen() // 调用 doAfterOpen 方法,这可能是一些在遮罩层打开后需要执行的额外逻辑
    },

openModal() 打开modal,并将将其信息记录到 modalStack 数组中。

js 复制代码
// element/src/utils/popup/popup-manager.js 
/**
 * @returns Returns the modal DOM element, creating it if it doesn't exist.
 */
const getModal = function() {
  if (Vue.prototype.$isServer) return  // 检查是否在 Vue 服务端渲染环境中,如果是,则直接返回,不执行后续操作
  let modalDom = PopupManager.modalDom 
  if (modalDom) {
    hasModal = true
  } else {
    hasModal = false
    modalDom = document.createElement('div')
    PopupManager.modalDom = modalDom

    modalDom.addEventListener('touchmove', function(event) {
      event.preventDefault()
      event.stopPropagation()
    })

    modalDom.addEventListener('click', function() {
      PopupManager.doOnModalClick && PopupManager.doOnModalClick()
    })
  }

  return modalDom
}

const PopupManager = {
  modalStack: [], // 跟踪当前打开的遮罩层的数组。 它维护有关每个遮罩层关联的组件的信息,例如其 ID、zIndex 和 modalClass. 
  
    /**
   * @returns Returns the next available zIndex for a modal.
   */
  nextZIndex: function() {
    return PopupManager.zIndex++
  },

  // popupid,层级,dom,遮罩层class,遮罩层过渡动画(Boolean)默认为true
  openModal: function(id, zIndex, dom, modalClass, modalFade) {
	if (Vue.prototype.$isServer) return // 检查是否在 Vue 服务端渲染环境中,如果是,则直接返回,不执行后续操作。
    if (!id || zIndex === undefined) return // 检查传入的参数 id 和 zIndex 是否存在,如果不存在,则直接返回,不执行后续操作。
    const modalStack = this.modalStack;
	// 遍历了 modalStack 数组,检查是否已经存在具有相同 id 的遮罩层,如果存在,则直接返回,不执行后续操作。
    for (let i = 0, j = modalStack.length; i < j; i++) {
      const item = modalStack[i];
      if (item.id === id) {
        return;
      }
    }

    const modalDom = getModal(); // 获取modal DOM element
	//遮罩层添加样式 过渡动画 移除动画
    addClass(modalDom, 'v-modal');
    if (this.modalFade && !hasModal) {
      addClass(modalDom, 'v-modal-enter');
    }
    if (modalClass) {
      let classArr = modalClass.trim().split(/\s+/);
      classArr.forEach(item => addClass(modalDom, item));
    }
      
    setTimeout(() => {
      removeClass(modalDom, 'v-modal-enter');
    }, 200);
      
	// 检查一个 DOM 元素(dom)的父节点是否不是一个文档片段(nodeType 为 11)。
    if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
      dom.parentNode.appendChild(modalDom);
    } else {
       // 未指定父元素,则添加到 document.body 中。
      document.body.appendChild(modalDom);
    }

    if (zIndex) {
      modalDom.style.zIndex = zIndex;
    }
    //...
    // 将modalDom的相关信息(id、zIndex、modalClass)添加到 modalStack 数组中。  
    this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });
  },
}

2. 点击Drawer组件范围外,关闭当前Drawer 的实现。

最开始研究公司项目自定义的抽屉组件时,发现原来的实现方式是给每一个抽屉组件在最外层包裹一个div 作为遮罩层,并通过addEventListener监听点击事件来控制页面关闭。但是这样不是很优雅,于是开始了对element-ui组件Drawer组件的modal实现的研究。

2.1 公司自定义抽屉组件的优化实现:

研究完Modal的实现后,我考虑直接引入了elementUIpopup/index.jspopup-manage.js文件 ,开始尽可能避免去修改原有抽屉组件的代码,设想实现如下:

  • 通过点击遮罩层时触发popup-manage中的click事件监听器,执行关闭modal的函数(doOnModalClick()),
  • 然后在该函数中调用实例拥有的close()关闭自定义抽屉组件。同时在beforeDestory()中触发PopupManager.closeModal(this._popupId)来关闭modal遮罩层。

然后又回头去看了看element-ui是如何实现的。

2.2 element-ui的实现总结:

在抽屉模板设计时elementUI多包了一层div class="el-drawer__container" 给这个div添加了@click.self="handleWrapperClick"

由于抽屉实际上占满了全屏,所以其实不管是否展示modal层,点击抽屉组件范围外的地方实际上都是触发handleWrapperClick() 去关闭抽屉组件。

然后在visible值变成false时,触发popup/index.js 中visible的监听回调方法,这个方法会依次执行close(),this.doClose(),this.doAfterClose() ,最后通过PopupManager.closeModal(this._popupId)关闭modal层。

modal相关源码参考

相关推荐
Мартин.2 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。3 小时前
案例-表白墙简单实现
前端·javascript·css
数云界3 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd3 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常3 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer3 小时前
Vite:为什么选 Vite
前端
小御姐@stella3 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing3 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd3 小时前
前端知识汇总(持续更新)
前端
万叶学编程6 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js