element-ui源码所得
首先看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 'element-ui/src/utils/popup';
import Migrating from 'element-ui/src/mixins/migrating';
import emitter from 'element-ui/src/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() {
let 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');
},
//在父组件中使用@opened
afterEnter() {
this.$emit('opened');
},
//在父组件中使用@closed
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>
在watch中监听了vue根dom元素的滑动事件,每次滑动都会触发this.updatePopper这个方法
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++;
});
}
}
}
},
this.broadcast是emitter混入的方法
updatePopper() {
this.broadcast('ElSelectDropdown', 'updatePopper');
this.broadcast('ElDropdownMenu', 'updatePopper');
},
下面我们又来看看emitter.js看看是怎么封装的(通过代码注释在上面详细解释了作用)
/**
* 广播方法定义(向指定子孙组件传递)
* @param {String} componentName 组件名称
* @param {String} eventName 事件名称
* @param {Object} params 参数
*/
function broadcast(componentName, eventName, params) {
//遍历组件中的子组件
this.$children.forEach(child => {
//vm.$options 用于当前 Vue 实例的初始化选项。需要在选项中包含自定义 property 时会有用处:
var name = child.$options.componentName;
//递归确保每一个孙和子组件都能接收到
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
/**
* 派发方法定义(向指定父爷组件传递)
* @param {String} componentName 组件名称
* @param {String} eventName 事件名称
* @param {Object} params 参数
*/
dispatch(componentName, eventName, params) {
//this.$root当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
//this.$parent父实例,如果当前实例有的话。
var parent = this.$parent || this.$root;
//componentName为element组件自定义的组件名字
var name = parent.$options.componentName;
//while循环找到与传入的componentName 名字相同的父级之后停止调用
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
//parent不为空代表还有父组件则继续向上寻找
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
看完了这个混用我直接小刀喇屁股开眼了,没想到后面还有让我震惊的,既然上面废了老大劲就为了调用eventName(updatePopper)我就想看看这个updatePopper到底是何方神圣。根据之前传入的componentName顺势来到了select-dropdown.vue下拉组件
<template>
<div
class="el-select-dropdown el-popper"
:class="[{ 'is-multiple': $parent.multiple }, popperClass]"
:style="{ minWidth: minWidth }">
<slot></slot>
</div>
</template>
<script type="text/babel">
import Popper from 'element-ui/src/utils/vue-popper';
export default {
name: 'ElSelectDropdown',
//自定义的组件名字
componentName: 'ElSelectDropdown',
//混用
mixins: [Popper],
props: {
placement: {
default: 'bottom-start'
},
boundariesPadding: {
default: 0
},
popperOptions: {
default() {
return {
gpuAcceleration: false
};
}
},
visibleArrow: {
default: true
},
appendToBody: {
type: Boolean,
default: true
}
},
data() {
return {
minWidth: ''
};
},
computed: {
popperClass() {
return this.$parent.popperClass;
}
},
watch: {
'$parent.inputWidth'() {
this.minWidth = this.$parent.$el.getBoundingClientRect().width + 'px';
}
},
mounted() {
this.referenceElm = this.$parent.$refs.reference.$el;
this.$parent.popperElm = this.popperElm = this.$el;
this.$on('updatePopper', () => {
if (this.$parent.visible) this.updatePopper();
});
this.$on('destroyPopper', this.destroyPopper);
}
};
</script>
updatePopper方法存在于select-dropdown.vue混用的Popper中,vue-popper.js则是elementui的一个管家,主要管理这弹窗的z-index确保弹窗的层级正确,看到这儿我不禁感叹elementui工程化是真滴高啊,一个简简单单的混用就用的如此出神入化,如此高的复用率,看着我项目中高度耦合的混入就觉得人与人差距怎么这么大,哈哈哈哈哈。
弹窗组件总结
1.$on 与 $emit的使用
2.通过改变key销毁div
3.组件之间的派发与广播
这些都可以使用在我们自己的项目中