vue2class写法源码分析vue-property-decorator(2)

716 阅读4分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

1 前言

前面写了一篇vue2class 写法源码分析 vue-class-component(1), 但是光有那个库是还不够的, 因为它只有一个类装饰器, 只提供了选项中的data,methods,computed的处理,对于watchprops等都没有处理, 这些都将在这个库中解决

在这个库中, 导出了一堆方法装饰器

2 再看 vue-class-component

它还导出了两个方法

2.1 mixins

export function mixins(...Ctors: VueClass<Vue>[]): VueClass<Vue> {
  return Vue.extend({
    // mixins的是一个函数的话, 会被认为是 Vue 的子类, 真正 mixins 的是 Ctor.options
    mixins: Ctors,
  })
}

2.2 createDecorator

vue-class-component导出了这个函数, 提供注册在处理完类装饰器后生成options时的回调

export function createDecorator(
  factory: (options: ComponentOptions<Vue>, key: string, index: number) => void
): VueDecorator {
  // 传入的函数, 作为 push 到 __decorators__ 中的回调
  // 这个返回的函数会作为 装饰器函数, 它没有返回值
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    // target 是原型对象, target.constructor 指向构造函数, 就是 @Component 装饰的类
    const Ctor =
      typeof target === 'function'
        ? (target as DecoratedClass)
        : (target.constructor as DecoratedClass)
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    // 保存起这个回调函数, 它的用处, 在上篇文章中有写
    Ctor.__decorators__.push((options) => factory(options, key, index))
  }
}

3 vue-property-decorator 导出了好多方法装饰器

3.1 Emit

// 匹配 `非单词边界` 后 `跟大写字母`  'aB' => 'B', 'AB' => 'B', 'ABC' => 'B'和'C', 'ABC DE' => 'B','C'和'E'
const hyphenateRE = /\B([A-Z])/g
const hyphenate = (str: string) => str.replace(hyphenateRE, '-$1').toLowerCase()

/**
 * decorator of an event-emitter function
 * @param  event The name of the event
 * @return MethodDecorator
 */
export function Emit(event?: string) {
  return function (_target: Vue, propertyKey: string, descriptor: any) {
    // 驼峰转连线符
    const key = hyphenate(propertyKey)
    // 保存旧函数
    const original = descriptor.value
    // 赋值新函数
    descriptor.value = function emitter(...args: any[]) {
      const emit = (returnValue: any) => {
        // 触发的事件名
        const emitName = event || key
        // 如果旧函数返回有值, 把它放在最前面
        returnValue ?? args.unshift(returnValue)
        // 触发事件
        this.$emit(emitName, ...args)
      }
      // 调用旧函数
      const returnValue: any = original.apply(this, args)
      // 如果旧函数返回 Promise
      if (isPromise(returnValue)) {
        returnValue.then(emit)
      } else {
        emit(returnValue)
      }

      return returnValue
    }
  }
}

function isPromise(obj: any): obj is Promise<any> {
  return obj instanceof Promise || (obj && typeof obj.then === 'function')
}

用法

<!-- 父组件 -->
<template>
  <div>
    <Emit @click="clickEmit" @fn="clickEmit"></Emit>
  </div>
</template>

<script lang="ts">
  import { Component, Vue } from 'vue-property-decorator'
  import Emit from './emit.vue'
  @Component({
    components: {
      Emit,
    },
  })
  class Demo extends Vue {
    clickEmit(...args) {
      console.log(args)
    }
  }
  export default Demo
</script>
<!-- 子组件 -->
<template>
  <div class="home">
    <div @click="emit('后面传的参')">emit 装饰器命名</div>
    <div @click="fn('后面传的参')">emit 函数命名</div>
  </div>
</template>

<script lang="ts">
  import { Component, Vue, Emit } from 'vue-property-decorator'
  @Component
  export default class Home extends Vue {
    @Emit('click')
    emit() {
      console.log('emit 装饰器命名')
      return '前面传的参'
    }
    @Emit()
    fn() {
      console.log('emit 函数命名, 触发 fn, 没有返回值')
    }
  }
</script>

image.png

3.1.1 对比

// 装饰器用法
import { Component, Vue, Emit } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @Emit('emit-demo')
  emitFn() {
    // ...
    console.log('emitFn')
    // ...
  }
}
// options 写法
export default {
  methods: {
    oldEmitFn() {
      // ...
      console.log('emitFn')
      // ...
    },
    emitFn(...args) {
      const returnValue = this.oldEmitFn(...args)
      const emit = (returnValue) => {
        returnValue ?? args.unshift(returnValue)
        // 只有这里不同, 如果 @emit() 传入了事件名就使用这个事件名, 否则使用函数名
        const emitName = 'emit-demo'
        this.$emit(emitName, ...args)
      }
      if (isPromise(returnValue)) {
        returnValue.then(emit)
      } else {
        emit(returnValue)
      }
    },
  },
}

3.2 Model

import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'

/**
 * decorator of model
 * @param  event event name
 * @param options options
 * @return PropertyDecorator
 */
export function Model(
  event?: string,
  options: PropOptions | Constructor[] | Constructor = {}
) {
  //
  return (target: Vue, key: string) => {
    // ==== 装饰器函数开始 ===
    const factory = (componentOptions, k) => {
      // 修改 props 和 model
      componentOptions.props || ((componentOptions.props = {}) as any)
      componentOptions.props[k] = options
      componentOptions.model = { prop: k, event: event || k }
    }
    createDecorator(factory)(target, key)
    // ==== 装饰器函数结束 ===
  }
}

3.2.1 对比

// 装饰器用法
import { Component, Vue, Model } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @Emit('event', { type: Object })
  eventName
}
// options 写法
export default {
  props: {
    event: { type: Object },
  },
  model: {
    prop: 'eventName',
    event: 'event',
  },
}

3.3 ModelSync

import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'

/**
 * decorator of synced model and prop
 * @param propName the name to interface with from outside, must be different from decorated property
 * @param  event event name
 * @param options options
 * @return PropertyDecorator
 */
export function ModelSync(
  propName: string,
  event?: string,
  options: PropOptions | Constructor[] | Constructor = {}
) {
  return (target: Vue, key: string) => {
    const factory = (componentOptions, k) => {
      componentOptions.props || ((componentOptions.props = {}) as any)
      componentOptions.props[propName] = options

      componentOptions.model = { prop: propName, event: event || k }
      // 比着 Model 多传入了一个 propName 多处理了一下 computed
      componentOptions.computed || (componentOptions.computed = {})
      componentOptions.computed[k] = {
        get() {
          return (this as any)[propName]
        },
        set(value: any) {
          this.$emit(event, value)
        },
      }
    }
    createDecorator(factory)(target, key)
  }
}

3.3.1 对比

// 装饰器写法
import { Component, Vue, ModelSync } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @ModelSync('propName', 'event', { type: Object })
  eventName!: any
}
// options写法
export default {
  props: {
    propName: { type: Object },
  },
  model: {
    prop: 'eventName',
    event: 'event',
  },
  computed: {
    eventName: {
      get() {
        return this.propsName
      },
      set(value) {
        this.$emit('event', value)
      },
    },
  },
}

3.4 Prop

import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'

/**
 * decorator of a prop
 * @param  options the options for the prop
 * @return PropertyDecorator | void
 */
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
  return (target: Vue, key: string) => {
    createDecorator((componentOptions, k) => {
      componentOptions.props || ((componentOptions.props = {}) as any)
      // 简简单单, 就是给 props 增加一个 属性, k 是装饰的方法名, options 是 Prop 的参数
      componentOptions.props[k] = options
    })(target, key)
  }
}

3.4.1 对比

// 装饰器写法
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @Prop({ type: Object })
  propName!: any
}
// options写法

export default {
  props: {
    propName: { type: Object },
  },
}

3.5 PropSync

import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue2/types/options'

/**
 * decorator of a synced prop
 * @param propName the name to interface with from outside, must be different from decorated property
 * @param options the options for the synced prop
 * @return PropertyDecorator | void
 */
export function PropSync(
  propName: string,
  options: PropOptions | Constructor[] | Constructor = {}
) {
  return (target: Vue, key: string) => {
    createDecorator((componentOptions, k) => {
      componentOptions.props || (componentOptions.props = {} as any)
      componentOptions.props[propName] = options

      componentOptions.computed || (componentOptions.computed = {})
      componentOptions.computed[k] = {
        get() {
          return (this as any)[propName]
        },
        set(this: Vue, value: any) {
          this.$emit(`update:${propName}`, value)
        },
      }
    })(target, key)
  }
}

3.5.1 对比

// 装饰器写法
import { Component, Vue, PropSync } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @PropSync('propName', { type: Object })
  computedKey!: any
}
// options写法

export default {
  props: {
    propName: { type: Object },
  },
  computed: {
    computedKey: {
      get() {
        return this.propName
      },
      set(value) {
        this.$emit('update:propName', value)
      },
    },
  },
}

3.6 Ref

import Vue from 'vue2'
import { createDecorator } from 'vue-class-component'

/**
 * decorator of a ref prop
 * @param refKey the ref key defined in template
 */
export function Ref(refKey?: string) {
  return createDecorator((options, key) => {
    options.computed = options.computed || {}
    options.computed[key] = {
      cache: false,
      get(this: Vue) {
        return this.$refs[refKey || key]
      },
    }
  })
}

3.6.1 对比

// 装饰器写法
import { Component, Vue, Ref } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @Ref('refKey')
  key!: any
}
// options写法

export default {
  computed: {
    key: {
      cache: false,
      get() {
        return this.$refs.refKey
      },
    },
  },
}

3.7 VModel

import Vue, { PropOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'

/**
 * decorator for capturings v-model binding to component
 * @param options the options for the prop
 */
export function VModel(options: PropOptions = {}) {
  const valueKey: string = 'value'
  return createDecorator((componentOptions, key) => {
    componentOptions.props || ((componentOptions.props = {}) as any)
    componentOptions.props[valueKey] = options

    componentOptions.computed || (componentOptions.computed = {})
    componentOptions.computed[key] = {
      get() {
        return (this as any)[valueKey]
      },
      set(this: Vue, value: any) {
        this.$emit('input', value)
      },
    }
  })
}

3.7.1 对比

// 装饰器写法
import { Component, Vue, VModel } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @VModel({ type: Object })
  computedKey!: any
}
// options写法

export default {
  props: {
    value: {
      type: Object,
    },
  },
  computed: {
    computedKey: {
      get() {
        return this.value
      },
      set(value) {
        this.$emit('input', value)
      },
    },
  },
}

3.8 Watch

import { WatchOptions } from 'vue2'
import { createDecorator } from 'vue-class-component'

/**
 * decorator of a watch function
 * @param  path the path or the expression to observe
 * @param  WatchOption
 * @return MethodDecorator
 */
export function Watch(path: string, options: WatchOptions = {}) {
  const { deep = false, immediate = false } = options

  return createDecorator((componentOptions, handler) => {
    if (typeof componentOptions.watch !== 'object') {
      componentOptions.watch = Object.create(null)
    }

    const watch: any = componentOptions.watch

    if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
      watch[path] = [watch[path]]
    } else if (typeof watch[path] === 'undefined') {
      watch[path] = []
    }

    watch[path].push({ handler, deep, immediate })
  })
}

3.8.1 对比

// 装饰器写法
import { Component, Vue, Watch } from 'vue-property-decorator'
@Component
export default class Demo extends Vue {
  @Watch('path', { deep: true })
  watchFn() {
    // ...
  }
}
// options写法

export default {
  watch: {
    path: {
      ...{ deep: true },
      handler: this.watchFn,
    },
  },
  methods: {
    watchFn() {
      // ...
    },
  },
}

4 总结

常用的就俩,Prop,Watch, 其它的还不如直接用options写法

它就是能帮我们更好得类型推导, 可以脱离options的上下跳

举个例子就明白了, 如果看掘金, 没有右侧边导航, 只能上下滚动, 那是多么的麻烦, class 写法可以很好的类型推导, 在编辑器中, 可以快速定位

想象一下如果没有这个导航定位

image.png

5 最后

下一篇计划写 模块化, 敬请期待, 感觉这篇文章能改到你启发的, 希望给个点赞, 评论, 收藏, 关注...

按照惯例, 附上之前写的几篇文章

  1. vue2 源码解析之 nextTick
  2. 代码片段之 js 限流调度器
  3. 数据结构与算法之链表(1)
  4. vue2 源码解析之事件系统$on
  5. vue2-全局 api 源码分析
  6. vue2class 写法源码分析 vue-class-component(1)
  7. js 原生语法之 prototype,proto和 constructor
  8. js 原生语法之继承及实现方式