在Vue中如何将通用的方法抽象出来进行封装,mixin是一种方式,但是mixin的缺点也很明显,关于mixin的缺点这里不做过多介绍,也可以通过这篇文章介绍的通过装饰器对通用列表的分装也是一种比较好的处理方式。
先定义一个基础的数据上下文,作为对数据的操作逻辑的抽象,以达到对逻辑的复用。
- import { getEvents } from './attachEvent'
-
- class DataContext {
- @getEvents
- events
- }
-
- export { DataContext }
- export const getEvents = (target, key) => {
- return {
- get () {
- 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)

再定义一个表格通用的类主要目的是为了将通用列表的基础通用逻辑进行封装
- 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 }
给类的成员方法绑定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())

在 Vue 中基本上是通过 $emit 方法触发自定义事件的,但是在 .vue 文件中如何触发 .js 文件中定义的这些方法呢,这里对 TableDataContext 里面的 handleFieldWidthChanged 的方法进行改动,为方法绑定一个装饰器
- import { attachTableEvent } from './attachEvent'
-
- class TableDataContext extends DataContext {
-
- @bind
- @attachTableEvent('resize-change')
- handleFieldWidthChanged() {
- console.log('change')
- }
- }
-
- export { TableDataContext }
给 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
- }
- }
- }

如何在访问 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)

这里可以看到对同一个方法绑定多个装饰器执行的先后顺序。
接下来是对于基础列表的封装让我们定义的这些被装饰器修饰的方法可以在 .vue 文件里面被使用
- <TableContextAdapter :context="context" />
- <script>
- import TableContextAdapter from './TableContextAdapter.vue'
- import { TableDataContext } from '@/components/table/tableDataContext'
-
- export default {
- components: {
- TableContextAdapter
- },
- data() {
- return {
- /**
- * 表格数据与操作上下文。
- */
- context: new TableDataContext()
- }
- }
- }
- script>
- <BaseTable v-on="combinedEvents" />
-
- <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>
- <div>
- <div @click="handleResizableChange">resize-changediv>
- <div @click="handleIssueChange">issue-changediv>
- <div @click="handleIssueDelete">issue-deletediv>
- <div @click="handleIssueModify">issue-modifydiv>
- div>
-
- <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 }