拯救还在使用vue2+element-ui的小伙伴——tooltip内存泄露的问题

213 阅读2分钟

懒的写文案,我就直接上代码了, 解决了内存泄露的问题,文档在最下方!还在使用vue2+element-ui的直接贴过去用哈,记得点个赞赞~~~

MyTooltip 组件为二次封装的组件

  • 使用示例
// 1. 超出一行自动省略,且提示
<my-tooltip :rows="1"> 这是一段文本内容,这是一段文本内容 </my-tooltip>

// 2. 超出两行自动省略,且提示
<my-tooltip :rows="2"> 这是一段文本内容,这是一段文本内容 </my-tooltip>

// 3. 这里是自定义内容
<my-tooltip content="这里是自定义内容"> 这是一段文本内容,这是一段文本内容 </my-tooltip>

// 4. 这里是插槽自定义内容
<my-tooltip content="这里是自定义内容"> 
    <template #content>
        <div>
          #content插槽
          <br>
          换行
        </div>
    </template>
    这是一段文本内容,这是一段文本内容
</my-tooltip>

// 5. 点击按钮,控制tootlip显隐(需要定义`tooltipVisible` 变量
<el-button @click="tooltipVisible = !tooltipVisible">点击这里</jl-button>
<my-tooltip v-model="tooltipVisible">这是一段文本内容,这是一段文本内容</my-tooltip>
  • 组件代码
<template>
  <span
    class="my-tooltip"
    ref="textRef"
    :style="{
      '-webkit-line-clamp': rows,
      display: block ? '-webkit-box' : '-webkit-inline-box',
    }"
    :class="{
      'is-visible': !rows,
    }"
    @click="handleClick"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave">
    <slot></slot>
    <el-tooltip
      v-if="showPopper"
      ref="tooltipRef"
      effect="dark"
      :class="{ 'is-one-row': rows === 1 }"
      :popper-class="internalPopperClass"
      :content="hasContentSlot ? undefined : content || internalContent"
      :disabled="isDisabled"
      :enterable="enterable"
      :placement="placement"
      :visible-arrow="false">
      <template v-if="hasContentSlot" #content>
        <slot name="content"></slot>
      </template>
    </el-tooltip>
  </span>
</template>

<script>
export default {
  name: 'MyTooltip',
  inheritAttrs: false,
  props: {
    value: { type: Boolean, default: false },
    rows: { type: Number, default: 0 },
    block: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    enterable: { type: Boolean, default: true },
    content: { type: String, default: '' },
    placement: { type: String, default: 'top' },
    popperClass: { type: String, default: '' },
    hideOnClick: { type: Boolean, default: false },
  },
  watch: {
    async isHovering(newVal) {
      if (newVal) {
        this.showPopper = true;
      }
      await this.$nextTick();
      const popper = this.$refs.tooltipRef;
      const reference = this.$refs.textRef;
      if (!popper) return;
      if (newVal) {
        if (reference) {
          popper.referenceElm = reference;
          popper.popperJS && (popper.popperJS._reference = reference);
        }
        // 监听为了销毁
        this.showPopperUnwatcher = this.$watch(
          () => popper.showPopper,
          (isShowNewVal) => {
            this.showPopper = this.internalValue = isShowNewVal;
            if (isShowNewVal) {
              //
            } else {
              this.unwatchShowPopper();
            }
          }
        );
        popper.updatePopper && popper.updatePopper();
        popper.show && popper.show();
      } else {
        popper.hide && popper.hide();
        // this.unwatchShowPopper();
      }
    },
    async internalValue(newVal) {
      if (newVal) {
        this.handleMouseEnter();
      } else {
        this.handleMouseLeave();
      }
    },
  },
  computed: {
    internalValue: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit('input', value);
      },
    },
    internalPopperClass() {
      return ['custom-tooltip', this.popperClass].filter(Boolean).join(' ');
    },
    isDisabled() {
      return this.disabled || (!this.isOverflow && !!this.rows);
    },
    hasContentSlot() {
      return !!this.$slots.content;
    },
  },
  data() {
    return {
      isOverflow: false,
      internalContent: '',
      textEl: null,

      isHovering: false,
      showPopper: false,
      showPopperUnwatcher: null,
    };
  },
  methods: {
    getContent() {
      this.textEl = this.$refs.textRef;
      if (!this.textEl) return;
      this.internalContent = this.textEl.innerText;
    },
    handleMouseEnter() {
      this.getContent();
      this.checkOverflow();
      if (this.isDisabled) return;
      this.isHovering = this.internalValue = true;
    },
    async handleMouseLeave() {
      this.isHovering = false;
      if (!this.enterable) {
        this.showPopper = false;
      }
    },
    checkOverflow() {
      if (!this.textEl) return;
      this.isOverflow = this.textEl.scrollWidth - 1 > this.textEl.clientWidth || this.textEl.scrollHeight - 1 > this.textEl.clientHeight;
    },
    unwatchShowPopper() {
      if (this.showPopperUnwatcher) {
        this.showPopperUnwatcher();
        this.showPopperUnwatcher = null;
      }
    },
    handleClick() {
      if (this.hideOnClick) {
        this.showPopper = false;
      }
      this.$emit('click');
    },
  },
  beforeDestroy() {
    // 组件销毁时清理 watcher,防止内存泄漏
    this.unwatchShowPopper();
  },
};
</script>

<style lang="scss" scoped>
.my-tooltip {
  max-width: 100%;
  word-break: break-all;
  vertical-align: bottom;
  -webkit-box-orient: vertical;
  text-overflow: ellipsis;
  overflow: hidden;

  &.is-visible {
    overflow: visible;
  }
  &.is-one-row {
    white-space: nowrap;
  }
}
</style>
<style lang="scss">
.el-tooltip__popper {
  &.custom-tooltip {
    z-index: 99999 !important;
    max-width: 400px;
    max-height: 300px;
    overflow-y: auto;
    margin: 4px 0;
    .popper__arrow {
      display: none;
    }
  }
}
</style>

Attributes

参数说明类型可选值默认值
value / v-model控制 tooltip 显示与隐藏(受控)booleantrue / falsefalse
rows控制省略行数number0
content显示的内容string默认插槽中的内容
block是否块级元素(占一整行)booleantrue / falsefalse
enterable鼠标是否可进入到 tooltip 中booleantrue / falsefalse
disabled禁用booleantrue / falsefalse
placement位置stringtop/top-start/top-end/bottom/bottom-start/bottom-end/left/left-start/left-end/right/right-start/right-endtop
hide-on-click点击时是否隐藏 tooltipbooleantrue / falsefalse

Slot

name说明
content自定义tooltip提示内容