Skip to content

Latest commit

 

History

History
452 lines (378 loc) · 14.9 KB

vue-property-decorator源码解析.md

File metadata and controls

452 lines (378 loc) · 14.9 KB

vue-property-decorator源码解析

version 9.1.2 愣锤 2022/02/06

介绍

vue-property-decorator是对vue-class-component的进一步封装,让vue+ts开发时支持更多的装饰器写法,例如@Prop、@Watch等。

基本用法如下:

@Component
export default class YourComponent extends Vue {
  @Prop({ default: 'default value' }) readonly propB!: string

  @Provide('bar') baz = 'bar'

  @Inject() readonly foo!: string

  @Watch('child')
  private onChildChanged(val: string, oldVal: string) {}

  @Emit()
  onInputChange(e: Event) {
    return e.target.value
  }
}

工程构建

package.json文件中的main字段可知库的入口文件是lib/index.umd.js:

{
  "main": "lib/index.umd.js",
}

但是lib/index.umd.js在源码中并不存在,因此这一定是打包后的产物,所以我们再看构建相关的脚本命令:

{
  "scripts": {
    "build": "tsc -p ./src/tsconfig.json && rollup -c"
  }
}

从命令可知构建逻辑是先通过tsc命令编译ts文件,然后再通过rollup打包输出文件。再看rollup.config.js 文件的配置:

export default {
  input: 'lib/index.js',
  output: {
    file: 'lib/index.umd.js',
    format: 'umd',
    name: 'VuePropertyDecorator',
    globals: {
      vue: 'Vue',
      'vue-class-component': 'VueClassComponent',
    },
    exports: 'named',
  },
  external: ['vue', 'vue-class-component', 'reflect-metadata'],
}

由此可知打包的入口lib/index.js也就是源码程序的入口, 输出地址是lib/index.umd.js,这也就和package.json文件中的main字段对应上了。

源码解析

入口文件内容如下:

/** vue-property-decorator verson 9.1.2 MIT LICENSE copyright 2020 kaorun343 */
/// <reference types='reflect-metadata'/>
import Vue from 'vue'
import Component, { mixins } from 'vue-class-component'

export { Component, Vue, mixins as Mixins }

export { Emit } from './decorators/Emit'
export { Inject } from './decorators/Inject'
export { InjectReactive } from './decorators/InjectReactive'
export { Model } from './decorators/Model'
export { ModelSync } from './decorators/ModelSync'
export { Prop } from './decorators/Prop'
export { PropSync } from './decorators/PropSync'
export { Provide } from './decorators/Provide'
export { ProvideReactive } from './decorators/ProvideReactive'
export { Ref } from './decorators/Ref'
export { VModel } from './decorators/VModel'
export { Watch } from './decorators/Watch'

从上面的入口文件可以看到,该库直接从vue-class-component直接导出了Component、Mixins方法,然后导出了实现的Prop、Emit、Inject、Provide、Watch等装饰器。源码结构相对简单,就是直接导出了一系列装饰器方法。

vue-class-component的插件原理

在具体分析vue-property-decorator装饰器原理之前,先看下vue-class-component的插件原理,因为vue-property-decorator是依赖vue-class-component暴露的创建装饰器的方法来封装新的装饰器的。

在我们分析vue-class-component源码的时候,我们知道vue-class-component本质原理就是在处理类上静态属性/方法、实例属性/方法等,转化成vue实例化所需要的options参数。但是在处理options的时候,其中有下面这点一段代码,我们是只提到它是用于处理例如vue-property-decorator等库的装饰器的:

// decorate options
const decorators = (Component as DecoratedClass).__decorators__

if (decorators) {
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
}

这段代码就是判断Component装饰的原始类上是否存在__decorators__属性(值是一个全是函数的数组,本质是一系列用于创建装饰器的工程函数),如果存在的话就依次调用数组的每一项,并把options参数的控制权交给当前项函数,这样的话外界就能力操作options参数了。

但是Component装饰的原始类本身是不会携带__decorators__属性的,只有在使用了例如vue-property-decorator库暴露的装饰器时,才会在断Component装饰的原始类上添加__decorators__属性。

之所以使用vue-property-decorator库的装饰器后会在Component装饰的原始类上添加__decorators__属性,是因为vue-property-decorator的装饰器中会使用vue-class-component暴露的创建装饰器的方法,该方法会在Component装饰的原始类上添加__decorators__属性。看下vue-class-component暴露的工具方法:

/**
 * 一个抽象工厂函数,用于创建装饰器工厂
 * @param factory 用于创建装饰器的工厂
 */
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    // 获取Component装饰的类的构造函数
    const Ctor = typeof target === 'function'
      ? target as DecoratedClass
      : target.constructor as DecoratedClass
    // 如果该类上不存在__decorators__属性,则设置默认值
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    // 在__decorators__加入处理装饰器的逻辑
    Ctor.__decorators__.push(options => factory(options, key, index))
  }
}

整体逻辑,咱们比如以使用@Prop为例:

  • 在Component装饰的类上使用@Prop装饰器
  • @Prop装饰器的内部实现中调用了createDecorator方法,并传入处理@Prop的逻辑(也就是如何处理vue中的prop属性)
  • createDecorator方法会在@Component装饰的类上添加__decorators__属性,并在__decorators__数组中添加一项,该项是个函数,用于调用@Prop处理逻辑
  • @Component装饰器在执行时生成options参数后,会判断__decorators__是否存在,如果存在则依次调用其中的函数。
  • 因此也就通过钩子的调用实现了处理@Prop逻辑。

@Prop装饰器原理

@Prop装饰器的实现都在Prop.ts文件中,代码如下:

import Vue, { PropOptions } from 'vue'
import { createDecorator } from 'vue-class-component'
import { Constructor } from 'vue/types/options'
import { applyMetadata } from '../helpers/metadata'

/**
 * 封装的处理props属性的装饰器
 * @param  options @Prop(options)装饰器内的options参数选项
 * @return PropertyDecorator | void
 */
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
  return (target: Vue, key: string) => {
    // 如果@Prop(options)的options不存在type属性,
    // 则通过ts元数据获取@Prop装饰器装饰的属性的类型赋值给options.type
    applyMetadata(options, target, key)
    // createDecorator是工具方法
    // 参数才是真正处理prop的逻辑
    createDecorator((componentOptions, k) => {
      /**
       * 给vue-class-component生成的options.props[key]赋值为
       * @Prop的参数options,注意这里两处的options概念不同
       *
       * 再重复下概念:
       *  - componentOptions 是vue-class-component生成的options参数
       *  - k 是@Prop装饰器装饰的属性
       *  - options 是@Prop(options)装饰器的options参数
       */
      ;(componentOptions.props || ((componentOptions.props = {}) as any))[
        k
      ] = options
    })(target, key)
  }
}

// 判断是否支持ts元数据的Reflect.getMetadata功能
const reflectMetadataIsSupported =
  typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined'

// applyMetadata的实现
export function applyMetadata(
  options: PropOptions | Constructor[] | Constructor,
  target: Vue,
  key: string,
) {
  if (reflectMetadataIsSupported) {
    if (
      !Array.isArray(options) &&
      typeof options !== 'function' &&
      !options.hasOwnProperty('type') &&
      typeof options.type === 'undefined'
    ) {
      // 只有在装饰器参数为对象且不存在type属性时,
      // 才通过ts元数据获取数据类型给options.type赋值
      const type = Reflect.getMetadata('design:type', target, key)
      if (type !== Object) {
        options.type = type
      }
    }
  }
}

核心实现,就是通过applyMetadata处理@Prop参数没有type属性时,利用ts元数据获取@Prop装饰器装饰的属性的类型作为vue prop参数的type值。然后调用createDecorator创建prop处理的逻辑,处理逻辑更是直接把@Propoptions选项作为vue prop keyvalue值。

@Component
export default class MyComponent extends Vue {
  @Prop() age!: number
}

// 上述代码也就被转化成了下面的代码
// 是因为上述代码利用元数据获取age的类型是Number拼接为options.type,
// 然后将options赋值给props.age
export default {
  props: {
    age: {
      type: Number,
    },
  },
}

@Watch装饰器原理

先对比一下vue-property-decorator的watch写法和vue2的watch写法:

/**
 * vue-property-decorator的watch写法
 */
import { Vue, Component, Watch } from 'vue-property-decorator'

@Component
export default class YourComponent extends Vue {
  @Watch('person', {immediate: true, deep: true })
  private onPersonChanged1(val: Person, oldVal: Person) {
    // your code
  }
}

/**
 * vue2的watch写法,
 * 需要注意的是watch是支持数组写法的
 */
export default {
  watch: {
    'path-to-expression': [
      exprHandler,
      {
        handler() {
            // code...
        },
        deep: true,
        immediate: true,
      }
    ]
  }
}

@Watch装饰器就是要处理Component装饰的原生类中的watch语法。

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

/**
 * decorator of a watch function
 * @param  path the path or the expression to observe
 * @param  watchOptions
 */
export function Watch(path: string, watchOptions: WatchOptions = {}) {
  return createDecorator((componentOptions, handler) => {
    /**
     * 获取Component装饰的类上定义的watch参数,没有就赋默认值为空对象
     * 注意a ||= b的写法等同于 a = a || b
     */
    componentOptions.watch ||= Object.create(null)
    const watch: any = componentOptions.watch

    /**
     * 把watch监听的key的回调统一格式化成数组
     * watch的key的回调是支持string | Function | Object | Array的
     */
    if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
      watch[path] = [watch[path]]
    } else if (typeof watch[path] === 'undefined') {
      watch[path] = []
    }

    /**
     * 把 @Component 上 @Watch 装饰的函数和装饰器参数组合成vue的watch写法
     */
    watch[path].push({ handler, ...watchOptions })
  })
}

@Watch的实现还是利用createDecorator创建装饰器,其参数是具体的实现逻辑。具体逻辑就是获取@Component装饰的类上处理的watch参数,然后统一成数组格式,然后把@Watch装饰的函数逻辑以vue2的写法添加到其watch的数据的数组中。

@Ref装饰器原理

@Ref装饰器的源码实现如下:

import Vue from 'vue'
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废弃语法
      cache: false,
      // 通过计算属性的get返回$refs属性
      // 例如 @Ref() readonly myDom: HTMLDivElement
      // 返回的是this.$refs.myDom
      get(this: Vue) {
        // 优先取@Ref装饰器的参数,否则取属性属性名,作为ref的key 
        return this.$refs[refKey || key]
      },
    }
  })
}

核心实现还是获取@Component装饰的类上的computed属性,然后增加一个计算属性,通过计算属性的get返回一个this.$refs的正常写法。这里提一点,cache是已经废弃的语法,这里仍然保留只是向前兼容。

@Emit装饰器原理

import Vue from 'vue'

// Code copied from Vue/src/shared/util.js
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) {
    // 根据@Emit装饰器的函数名获取连字符的格式
    const key = hyphenate(propertyKey)
    const original = descriptor.value

    // 覆写装饰器函数的逻辑
    descriptor.value = function emitter(...args: any[]) {
      const emit = (returnValue: any) => {
        const emitName = event || key

        // 如果@Emit装饰的函数没有返回值,
        // 则直接emit装饰器函数的所有参数
        if (returnValue === undefined) {
          if (args.length === 0) {
            this.$emit(emitName)
          } else if (args.length === 1) {
            this.$emit(emitName, args[0])
          } else {
            this.$emit(emitName, ...args)
          }
        // 如果@Emit装饰的函数有返回值,
        // 则直接emit的值依次为:返回值、函数参数
        } else {
          args.unshift(returnValue)
          this.$emit(emitName, ...args)
        }
      }

      // 获取返回结果
      const returnValue: any = original.apply(this, args)

      // 如果是返回的promise则在then时emit
      // 否则直接emit
      if (isPromise(returnValue)) {
        returnValue.then(emit)
      } else {
        emit(returnValue)
      }

      return returnValue
    }
  }
}

// 判断是否是promise类型
// 判断手段为鸭式辨型
function isPromise(obj: any): obj is Promise<any> {
  return obj instanceof Promise || (obj && typeof obj.then === 'function')
}

@VModel装饰器原理

import Vue, { PropOptions } from 'vue'
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) => {
    // 给props.value赋值为装饰器参数
    ;(componentOptions.props || ((componentOptions.props = {}) as any))[
      valueKey
    ] = options
    // 给computed[被装饰的属性key]赋值为get/set
    // get时直接返回props.value, set时触发this.$emit('input')事件
    ;(componentOptions.computed || (componentOptions.computed = {}))[key] = {
      get() {
        return (this as any)[valueKey]
      },
      set(this: Vue, value: any) {
        this.$emit('input', value)
      },
    }
  })
}

其他

s 其他装饰器的实现基本大同小异,就不再过多介绍。