为什么使用 extends 而不是 mixins
mixins
选项接受一个 mixin 对象数组。这些 mixin 对象可以像普通的实例对象一样包含实例选项,它们将使用一定的选项合并逻辑与最终的选项进行合并。
extends
使一个组件可以继承另一个组件的组件选项。这主要是为了便于扩展单文件组件。
从实现角度来看,extends
几乎和 mixins
相同。通过 extends
指定的组件将会当作第一个 mixin 来处理。
然而,extends
和 mixins
表达的是不同的目标。mixins
选项基本用于组合功能,而 extends
则一般更关注继承关系。
以上是官方文档的说明,看起来 extends
和 mixins
好像差不多,用哪个都可以,但其实他们有一个很大的区别:作用范围就不同。
mixins
只能混合选项,执行选项合并策略。
<script>
export default {
// mixins 能起作用的地方
// 选项合并
}
<script>
extends
继承所有(template、script、style)。
template
执行模板替换,script
执行选项合并策略,style
执行样式追加,且支持继承自定义块。
<template>
<!-- extends 能起作用的地方 -->
<!-- 模板替换 -->
</template>
<script>
export default {
// extends 能起作用的地方
// 选项合并
}
<script>
<style>
/* extends 能起作用的地方 */
/* 样式追加 */
</style>
Vue.js 2 使用 extends 扩展 element-ui 组件
MyButton.vue
<script>
import { Button } from 'element-ui'
export default {
extends: Button
}
</script>
main.js
import ElementUI from 'element-ui'
import MyButton from './components/MyButton'
Vue.use(ElementUI)
Vue.component('ElButton', MyButton)
以上 MyButton.vue
就只是简单的继承了 ElButton
,没有加额外的扩展,并全局把 ElButton
替换成了 MyButton.vue
的实现。
此时 MyButton.vue
和 ElButton
一模一样,接下来我们就可以在 ElButton
的基础上扩展一些逻辑,比如:为 el-button 加上节流的限制,并加上一些样式。
当然也不能瞎扩展,首先要看看 ElButton
的源码,对于会被覆盖的选项,需要保证原逻辑的正常运行。
node_modules/element-ui/packages/button/src/button.vue
<template>
...
</template>
<script>
export default {
name: 'ElButton',
...
methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
为了省事我就只放部分源码了,核心是要覆盖 handleClick
方法。
直接上实现代码:
MyButton.vue
<script>
import { Button } from 'element-ui'
export default {
extends: Button,
props: {
interval: {
type: Number,
default: 200
}
},
data() {
return {
throttle: false
}
},
methods: {
// 覆盖 handleClick 方法
// handleClick(evt) {
// this.$emit('click', evt)
// }
handleClick(evt) {
// 节流
if (!this.throttle) {
this.throttle = true
this.$emit('click', evt)
setTimeout(() => {
this.throttle = false
}, this.interval)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-button {
border-radius: 0;
}
</style>
MyButton.vue
继承了 ElButton
,在支持 ElButton
完整功能的基础上,实现了点击节流功能:一定时间范围内(通过 props 传入 interval 属性,默认 200ms)只能点一次。<style>
里的样式也添加了。
解决 el-dialog
mousedown mouseup 目标不一致的问题
以上是小试牛刀,使用 extends 扩展 element-ui 组件可以解决一些 element-ui 组件的 bug,比如 el-dialog
、el-popover
、el-drawer
都有这样一个问题:在弹框内按下鼠标移动到弹框外松开,会触发弹框外(比如 el-dialog 的遮罩层)点击事件,即 mousedown 在弹框内,mouseup 在弹框外,反之亦然,均会触发遮罩层 click 事件(默认配置下就会执行 handleClose 关闭弹框)。
定位 el-dialog 源码可以看出,只监听了遮罩层(wrapper)的 click 事件,遮罩层仅触发 mousedown 或者 mouseup 都会触发 click 事件,无论 mousedown 和 mouseup 的目标(event.target
)是否同一个。
node_modules/element-ui/packages/dialog/src/component.vue
<template>
<transition>
<div
v-show="visible"
class="el-dialog__wrapper"
@click.self="handleWrapperClick">
...
</div>
</transition>
</template>
<script>
export default {
name: 'ElDialog',
methods: {
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;
}
}
},
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>
在基于 Vue 3 的 element-plus 中就没有这个问题了,从源码可以看出 element-plus 判断了 mousedown 和 mouseup 的目标(event.target
)都是当前目标(event.currentTarget
)才会去执行 onModalClick 方法(即 mousedown 和 mouseup 目标都在遮罩层才触发点击遮罩层关闭)。
node_modules/element-plus/packages/components/dialog/src/dialog.vue
<template>
<teleport>
<transition>
<el-overlay>
<div
role="dialog"
@click="overlayEvent.onClick"
@mousedown="overlayEvent.onMousedown"
@mouseup="overlayEvent.onMouseup"
>
...
</div>
</el-overlay>
</transition>
</teleport>
</template>
<script lang="ts" setup>
import { useDeprecated, useNamespace, useSameTarget } from '@element-plus/hooks'
import { useDialog } from './use-dialog'
const {
onModalClick,
} = useDialog(props, dialogRef)
const overlayEvent = useSameTarget(onModalClick)
</script>
node_modules/element-plus/packages/hooks/use-same-target/index.ts
import { NOOP } from '@vue/shared'
export const useSameTarget = (handleClick?: (e: MouseEvent) => void) => {
if (!handleClick) {
return { onClick: NOOP, onMousedown: NOOP, onMouseup: NOOP }
}
let mousedownTarget = false
let mouseupTarget = false
// refer to this https://javascript.info/mouse-events-basics
// events fired in the order: mousedown -> mouseup -> click
// we need to set the mousedown handle to false after click fired.
const onClick = (e: MouseEvent) => {
// if and only if
if (mousedownTarget && mouseupTarget) {
handleClick(e)
}
mousedownTarget = mouseupTarget = false
}
const onMousedown = (e: MouseEvent) => {
// marking current mousedown target.
mousedownTarget = e.target === e.currentTarget
}
const onMouseup = (e: MouseEvent) => {
// marking current mouseup target.
mouseupTarget = e.target === e.currentTarget
}
return { onClick, onMousedown, onMouseup }
}
借鉴 element-plus 的处理,我们同样可以使用 extends 扩展 element-ui 的 el-dialog,并全局将 ElDialog 组件替换为扩展后的组件。
MyDialog.vue
<script>
import { Dialog } from 'element-ui'
export default {
extends: Dialog,
data() {
return {
mousedownTarget: false,
mouseupTarget: false
}
},
mounted() {
this.$el.addEventListener('mousedown', this.onMousedown)
this.$el.addEventListener('mouseup', this.onMouseup)
},
methods: {
// 覆盖 handleWrapperClick 方法
// handleWrapperClick() {
// if (!this.closeOnClickModal) return;
// this.handleClose();
// },
handleWrapperClick() {
if (!this.closeOnClickModal) return
if (this.mousedownTarget && this.mouseupTarget) {
this.handleClose()
}
this.mousedownTarget = this.mouseupTarget = false
},
onMousedown(e) {
this.mousedownTarget = e.target === e.currentTarget
},
onMouseup(e) {
this.mouseupTarget = e.target === e.currentTarget
}
}
}
</script>
main.js
import ElementUI from 'element-ui'
import MyDialog from './components/MyDialog'
Vue.use(ElementUI)
Vue.component('ElDialog', MyDialog)
MyDialog.vue
的处理和 element-plus 差不多,监听 mousedown 和 mouseup 事件,设置相应触发标志,覆盖 handleWrapperClick 方法,mousedown 和 mouseup 的目标(event.target
)都是当前目标(event.currentTarget
)才执行 handleClose 方法。