elementUi tooltip组件渲染分析

1,076 阅读2分钟

今天来看下 element tooltip组价 大致一个渲染的逻辑

一些东西是之前也有分析过就不提了 可以看之前的文章

// OK  先看初始化 beforeCreate
import Popper from '@/utils/vue-popper';
import { debounce } from 'throttle-debounce';
import {
  addClass, removeClass, on, off,
} from '@/utils/dom';
import { generateId } from '@/utils/util';
import Vue from 'vue';

export default {
  name: 'ElTooltip',

  mixins: [Popper],

  props: {
    openDelay: {
      type: Number,
      default: 0,
    },
    disabled: Boolean,
    manual: Boolean,
    effect: {
      type: String,
      default: 'dark',
    },
    arrowOffset: {
      type: Number,
      default: 0,
    },
    popperClass: String,
    content: String,
    visibleArrow: {
      default: true,
    },
    transition: {
      type: String,
      default: 'el-fade-in-linear',
    },
    popperOptions: {
      default() {
        return {
          boundariesPadding: 10,
          gpuAcceleration: false,
        };
      },
    },
    enterable: {
      type: Boolean,
      default: true,
    },
    hideAfter: {
      type: Number,
      default: 0,
    },
    tabindex: {
      type: Number,
      default: 0,
    },
  },

  data() {
    return {
      tooltipId: `el-tooltip-${generateId()}`,
      timeoutPending: null,
      focusing: false,
    };
  },
  // 开始
  beforeCreate() {
    if (this.$isServer) return;
    //  初始化了一个popperVM组件  声明了一个 node 属性
    this.popperVM = new Vue({
      data: { node: '' },
      render(h) {
        return this.node;
      },
    }).$mount();
    // 防抖 200 毫秒执行一次
    this.debounceClose = debounce(200, () => this.handleClosePopper());
    // 然后看下mounted
  },

  render(h) {
    // before的时候创建了
    if (this.popperVM) {
      // 之前node有声明了 那么现在赋值 就会触发响应 那这边模板就会被编译了 
      this.popperVM.node = (
        <transition
          name={ this.transition }
          onAfterLeave={ this.doDestroy }>
          <div
            onMouseleave={ () => { this.setExpectedState(false); this.debounceClose(); } }
            onMouseenter= { () => { this.setExpectedState(true); } }
            ref="popper"
            role="tooltip"
            id={this.tooltipId}
            aria-hidden={ (this.disabled || !this.showPopper) ? 'true' : 'false' }
            v-show={!this.disabled && this.showPopper}
            class={
              ['el-tooltip__popper', `is-${this.effect}`, this.popperClass]
            }>
            { this.$slots.content || this.content }
          </div>
        </transition>);
      // 这边的 context 是当前的这个实例this
      // 所以等下这边的 popper 会挂在到当前的 this.$refs.popper 下面
    }

    // 取到插槽的元素 是最后一个
    // 是一个 VNode
    const firstElement = this.getFirstElement();
    if (!firstElement) return null;

    const data = firstElement.data = firstElement.data || {};
    data.staticClass = this.addTooltipClass(data.staticClass);
    // 渲染这个插槽 接下去看mounted
    return firstElement;
  },

  mounted() {
    // 我们先看下渲染函数 看完我们可以看出这个就是那个插槽元素
    this.referenceElm = this.$el;
    // 元素节点
    if (this.$el.nodeType === 1) {
      // 设置属性
      this.$el.setAttribute('aria-describedby', this.tooltipId);
      this.$el.setAttribute('tabindex', this.tabindex);
      // 设置事件
      on(this.referenceElm, 'mouseenter', this.show);
      on(this.referenceElm, 'mouseleave', this.hide);
      on(this.referenceElm, 'focus', () => {
        if (!this.$slots.default || !this.$slots.default.length) {
          this.handleFocus();
          return;
        }
        const instance = this.$slots.default[0].componentInstance;
        if (instance && instance.focus) {
          instance.focus();
        } else {
          this.handleFocus();
        }
      });
      on(this.referenceElm, 'blur', this.handleBlur);
      on(this.referenceElm, 'click', this.removeFocusing);
    }
    // fix issue https://github.com/ElemeFE/element/issues/14424
    if (this.value && this.popperVM) {
      // 我们就直接看吧   基本就是一个显示   其他也没什么逻辑
      // 然后显示的话  要在下次事件环  因为这里面需要获取到 this.$refs.popper
      this.popperVM.$nextTick(() => {
        // 如果是显示状态的 那么就调用显示
        if (this.value) {
          // 这个是mixins Popper 注入的   我们看下
          this.updatePopper();
        }
      });
    }
  },
  watch: {
    focusing(val) {
      if (val) {
        addClass(this.referenceElm, 'focusing');
      } else {
        removeClass(this.referenceElm, 'focusing');
      }
    },
  },
  methods: {
    show() {
      this.setExpectedState(true);
      this.handleShowPopper();
    },

    hide() {
      this.setExpectedState(false);
      this.debounceClose();
    },
    handleFocus() {
      this.focusing = true;
      this.show();
    },
    handleBlur() {
      this.focusing = false;
      this.hide();
    },
    removeFocusing() {
      this.focusing = false;
    },

    addTooltipClass(prev) {
      if (!prev) {
        return 'el-tooltip';
      }
      return `el-tooltip ${prev.replace('el-tooltip', '')}`;
    },

    handleShowPopper() {
      if (!this.expectedState || this.manual) return;
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        this.showPopper = true;
      }, this.openDelay);

      if (this.hideAfter > 0) {
        this.timeoutPending = setTimeout(() => {
          this.showPopper = false;
        }, this.hideAfter);
      }
    },

    handleClosePopper() {
      if (this.enterable && this.expectedState || this.manual) return;
      clearTimeout(this.timeout);

      if (this.timeoutPending) {
        clearTimeout(this.timeoutPending);
      }
      this.showPopper = false;

      if (this.disabled) {
        this.doDestroy();
      }
    },

    setExpectedState(expectedState) {
      if (expectedState === false) {
        clearTimeout(this.timeoutPending);
      }
      this.expectedState = expectedState;
    },

    getFirstElement() {
      const slots = this.$slots.default;
      if (!Array.isArray(slots)) return null;
      let element = null;
      for (let index = 0; index < slots.length; index++) {
        if (slots[index] && slots[index].tag) {
          element = slots[index];
        }
      }
      return element;
    },
  },

  beforeDestroy() {
    this.popperVM && this.popperVM.$destroy();
  },

  destroyed() {
    const reference = this.referenceElm;
    if (reference.nodeType === 1) {
      off(reference, 'mouseenter', this.show);
      off(reference, 'mouseleave', this.hide);
      off(reference, 'focus', this.handleFocus);
      off(reference, 'blur', this.handleBlur);
      off(reference, 'click', this.removeFocusing);
    }
  },
};

vue-popper

  • updatePopper
import Vue from 'vue';
import {
  PopupManager,
} from '@/utils/popup';

// 依赖这个 popper 库  版本应该是1.x的  然后来看下updatePopper 做了什么
const PopperJS = Vue.prototype.$isServer ? function () {} : require('./popper');

const stop = (e) => e.stopPropagation();

/**
 * @param {HTMLElement} [reference=$refs.reference] - The reference element used to position the popper.
 * @param {HTMLElement} [popper=$refs.popper] - The HTML element used as popper, or a configuration used to generate the popper.
 * @param {String} [placement=button] - Placement of the popper accepted values: top(-start, -end), right(-start, -end), bottom(-start, -end), left(-start, -end)
 * @param {Number} [offset=0] - Amount of pixels the popper will be shifted (can be negative).
 * @param {Boolean} [visible=false] Visibility of the popup element.
 * @param {Boolean} [visible-arrow=false] Visibility of the arrow, no style.
 */
export default {
  props: {
    transformOrigin: {
      type: [Boolean, String],
      default: true,
    },
    placement: {
      type: String,
      default: 'bottom',
    },
    boundariesPadding: {
      type: Number,
      default: 5,
    },
    reference: {},
    popper: {},
    offset: {
      default: 0,
    },
    value: Boolean,
    visibleArrow: Boolean,
    arrowOffset: {
      type: Number,
      default: 35,
    },
    appendToBody: {
      type: Boolean,
      default: true,
    },
    popperOptions: {
      type: Object,
      default() {
        return {
          gpuAcceleration: false,
        };
      },
    },
  },

  data() {
    return {
      showPopper: false,
      currentPlacement: '',
    };
  },

  watch: {
    value: {
      immediate: true,
      handler(val) {
        this.showPopper = val;
        this.$emit('input', val);
      },
    },

    showPopper(val) {
      if (this.disabled) return;
      val ? this.updatePopper() : this.destroyPopper();
      this.$emit('input', val);
    },
  },

  methods: {
    createPopper() {
      if (this.$isServer) return;
      // 获取这个位置
      this.currentPlacement = this.currentPlacement || this.placement;
      if (!/^(top|bottom|left|right)(-start|-end)?$/g.test(this.currentPlacement)) {
        return;
      }

      const options = this.popperOptions;
      // 获取这个 popper元素 也就是弹出来的那个
      const popper = this.popperElm = this.popperElm || this.popper || this.$refs.popper;
      // 这个是触发的元素
      let reference = this.referenceElm = this.referenceElm || this.reference || this.$refs.reference;

      if (!reference
        && this.$slots.reference
        && this.$slots.reference[0]) {
        reference = this.referenceElm = this.$slots.reference[0].elm;
      }
      if (!popper || !reference) return;
      // 添加三角形 就是边框边上那个三角形
      if (this.visibleArrow) this.appendArrow(popper);
      // 添加到dom上面
      if (this.appendToBody) document.body.appendChild(this.popperElm);
      // 之前有的销毁了
      if (this.popperJS && this.popperJS.destroy) {
        this.popperJS.destroy();
      }
      // 设置下options
      options.placement = this.currentPlacement;
      options.offset = this.offset;
      options.arrowOffset = this.arrowOffset;
      // 传进去触发的  显示的  选项就行了    一些位置变化什么的就这个库内部来完成
      this.popperJS = new PopperJS(reference, popper, options);
      // onCreate  创建完之后的回调
      // eslint-disable-next-line no-unused-vars
      // 创建完成的时候
      this.popperJS.onCreate((_) => {
        this.$emit('created', this);
        // 更新下 transformOrigin  固定下过度变化位置
        this.resetTransformOrigin();
        this.$nextTick(this.updatePopper);
      });
      // 如果有传进来更新的函数 那就监听
      if (typeof options.onUpdate === 'function') {
        this.popperJS.onUpdate(options.onUpdate);
      }
      // 层级设置下 PopupManager Ele组件内部的弹层管理
      this.popperJS._popper.style.zIndex = PopupManager.nextZIndex();
      this.popperElm.addEventListener('click', stop);
    },

    // 顾名思义 更新这个Popper
    updatePopper() {
      const { popperJS } = this;
      // 有就更新 位置层级  没有就创建
      // 第一次没有
      if (popperJS) {
        popperJS.update();
        if (popperJS._popper) {
          popperJS._popper.style.zIndex = PopupManager.nextZIndex();
        }
      } else {
        // 第一次 没有就创建   看下createPopper
        this.createPopper();
      }
    },

    doDestroy(forceDestroy) {
      /* istanbul ignore if */
      if (!this.popperJS || (this.showPopper && !forceDestroy)) return;
      this.popperJS.destroy();
      this.popperJS = null;
    },

    destroyPopper() {
      if (this.popperJS) {
        this.resetTransformOrigin();
      }
    },

    resetTransformOrigin() {
      if (!this.transformOrigin) return;
      const placementMap = {
        top: 'bottom',
        bottom: 'top',
        left: 'right',
        right: 'left',
      };
      const placement = this.popperJS._popper.getAttribute('x-placement').split('-')[0];
      const origin = placementMap[placement];
      this.popperJS._popper.style.transformOrigin = typeof this.transformOrigin === 'string'
        ? this.transformOrigin
        : ['top', 'bottom'].indexOf(placement) > -1 ? `center ${origin}` : `${origin} center`;
    },
    // 加上箭头
    appendArrow(element) {
      let hash;
      if (this.appended) {
        return;
      }

      this.appended = true;

      // eslint-disable-next-line no-restricted-syntax
      for (const item in element.attributes) {
        if (/^_v-/.test(element.attributes[item].name)) {
          hash = element.attributes[item].name;
          break;
        }
      }

      const arrow = document.createElement('div');

      if (hash) {
        arrow.setAttribute(hash, '');
      }
      arrow.setAttribute('x-arrow', '');
      arrow.className = 'popper__arrow';
      element.appendChild(arrow);
    },
  },

  beforeDestroy() {
    this.doDestroy(true);
    if (this.popperElm && this.popperElm.parentNode === document.body) {
      this.popperElm.removeEventListener('click', stop);
      document.body.removeChild(this.popperElm);
    }
  },

  // call destroy in keep-alive mode
  deactivated() {
    this.$options.beforeDestroy[0].call(this);
  },
};