在Vue中如何将通用的方法抽象出来进行封装,mixin是一种方式,但是mixin的缺点也很明显,关于mixin的缺点这里不做过多介绍,也可以通过这篇文章介绍的通过装饰器对通用列表的分装也是一种比较好的处理方式。
DataContext
先定义一个基础的数据上下文,作为对数据的操作逻辑的抽象,以达到对逻辑的复用。
import { getEvents } from './attachEvent'
class DataContext {
@getEvents
events
}
export { DataContext }
getEvents
export const getEvents = (target, key) => {
return {
get () {
// target 指向 DataContext.prototype
console.log('访问了key ===> ' + key)
const cacheKey = `__${key}_events__`
return this[cacheKey] ?? (this[cacheKey] = {})
},
set () {}
}
}
对于装饰器不了解的这里不做过多的介绍,上述代码的意义是给 events 属性添加 get set 方法便于访问时会触发,此处的 target 指向的是实例的 __proto__
const data = new DataContext()
console.log('data.events ===> ', data.events)
console.log('data ===> ', data)
再定义一个表格通用的类主要目的是为了将通用列表的基础通用逻辑进行封装
TableDataContext
import { DataContext } from './dataContext'
import { getEvents } from './attachEvent'
import { bind } from './bind'
class TableDataContext extends DataContext {
@getEvents
tableEvents
constructor() {
super()
}
@bind
handleFieldWidthChanged() {
console.log('change')
}
}
export { TableDataContext }
bind
给类的成员方法绑定this。
const bind = (target, key, descriptor) => {
return {
configurable: true,
get() {
console.log('key ===> ', key)
console.log('target ===> ', target)
const boundFn = descriptor.value.bind(this)
Object.defineProperty(this, key, {
configurable: true,
get() {
return boundFn
}
})
return boundFn
}
}
}
调用 TableDataContext 查看 this上的属性和方法
const table = new TableDataContext()
console.log('table ===> ', table)
console.log(table.handleFieldWidthChanged())
attachTableEvent
在 Vue 中基本上是通过 $emit 方法触发自定义事件的,但是在 .vue 文件中如何触发 .js 文件中定义的这些方法呢,这里对 TableDataContext 里面的 handleFieldWidthChanged 的方法进行改动,为方法绑定一个装饰器
import { attachTableEvent } from './attachEvent'
class TableDataContext extends DataContext {
@bind
@attachTableEvent('resize-change')
handleFieldWidthChanged() {
console.log('change')
}
}
export { TableDataContext }
buildAttachEvent
给 tableEvents 用的预定义的 attachEvent
const attachTableEvent = buildAttachEvent('tableEvents')
const buildAttachEvent = propName => {
console.log('propName ===> ', propName)
return eventName => {
console.log('eventName ===> ', eventName)
return (target, key, descriptor) => {
console.log('target ===> ', target)
console.log('key ===> ', key)
console.log('descriptor ===> ', descriptor)
const privatePropName = `__${propName}__`
if (!Object.hasOwnProperty.call(target, privatePropName)) {
target[privatePropName] = { ...target[privatePropName] }
}
target[privatePropName][eventName] = key
return descriptor
}
}
}
buildEventsObject
如何在访问 tableEvents 属性时拿到绑定在 __tableEvents__ 这个自定义属性上的方法呢,添加一个 buildEventsObject 装饰器,并对 getEvents 进行如下改造
export const getEvents = (target, key) => {
return {
get () {
console.log('访问了key ===> ' + key)
const cacheKey = `__${key}_events__`
return this[cacheKey] ?? (this[cacheKey] = buildEventsObject(this, key))
},
set () {}
}
}
const buildEventsObject = (instance, propName) => {
console.log('instance ===> ', instance)
console.log('propName ===> ', propName)
const privatePropName = `__${propName}__`
const eventsMap = instance[privatePropName] ?? {}
console.log('eventsMap ==> ', eventsMap)
return Object.keys(eventsMap).reduce((acc, key) => {
acc[key] = instance[eventsMap[key]]
return acc
}, {})
}
const table = new TableDataContext()
console.log('table ===> ', table)
console.log('tableEvents ===> ', table.tableEvents)
这里可以看到对同一个方法绑定多个装饰器执行的先后顺序。
- 通过 getEvents 方法对基础类里面的属性 tableEvents 绑定 get 方法。
- 通过 buildAttachEvent('tableEvents') 在为属性 tableEvents 预定义执行方法。
- 访问属性 tableEvents 时会触发 get 方法,通过 buildEventsObject 获取绑定到属性 tableEvents 上的方法。
- 通过 bing 装饰器修正调用时的 this 的指向。
接下来是对于基础列表的封装让我们定义的这些被装饰器修饰的方法可以在 .vue 文件里面被使用
ListTable
<template>
<TableContextAdapter :context="context" />
</template>
<script>
import TableContextAdapter from './TableContextAdapter.vue'
import { TableDataContext } from '@/components/table/tableDataContext'
export default {
components: {
TableContextAdapter
},
data() {
return {
/**
* 表格数据与操作上下文。
*/
context: new TableDataContext()
}
}
}
</script>
TableContextAdapter
<template>
<BaseTable v-on="combinedEvents" />
</template>
<script>
import BaseTable from './BaseTable.vue'
import { TableDataContext } from '@/components/table/tableDataContext'
export default {
components: {
BaseTable
},
props: {
context: {
type: TableDataContext
}
},
/**
* 将 context 上的事件与该组件上的事件合并后传入 BaseTable
*/
computed: {
combinedEvents() {
// 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
const eventsObjList = [this.context.tableEvents, this.context.events, this.$listeners]
// [{ resize-change: ƒ, issue-change: ƒ }, { issue-modify: ƒ, issue-delete: ƒ }]
const eventNameList = [...new Set(eventsObjList.map(o => Object.keys(o)).flat())]
// ['resize-change', 'issue-change', 'issue-modify', 'issue-delete']
return eventNameList.reduce(
(map, name) =>
Object.assign(map, {
[name]: eventsObjList.map(eo => eo[name]).filter(f => f)
}),
{}
)
}
}
}
</script>
<style></style>
BaseTable
<template>
<div>
<div @click="handleResizableChange">resize-change</div>
<div @click="handleIssueChange">issue-change</div>
<div @click="handleIssueDelete">issue-delete</div>
<div @click="handleIssueModify">issue-modify</div>
</div>
</template>
<script>
export default {
created() {},
methods: {
handleResizableChange() {
this.$emit('resize-change')
},
handleIssueChange() {
this.$emit('issue-change')
},
handleIssueDelete() {
this.$emit('issue-delete')
},
handleIssueModify() {
this.$emit('issue-modify')
}
}
}
</script>
<style></style>
如果多个方法 emit 需要触发同一个方法只需要进行如下改写即可
import { DataContext } from './dataContext'
import { attachTableEvent, getEvents, attachEvent } from './attachEvent'
import { bind } from './bind'
class TableDataContext extends DataContext {
/**
* 直接监听到 VapdTable 上的事件。
*/
@getEvents
tableEvents
constructor() {
super()
}
@bind
@attachTableEvent('resize-change')
handleFieldWidthChanged() {
console.log('resize-change')
}
@bind
@attachEvent('issue-delete')
@attachEvent('issue-modify')
@attachTableEvent('issue-change')
handleIssueListChanged() {
console.log('issue-change')
}
}
export { TableDataContext }