今天来看下 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);
},
};