Fork me on GitHub

深入理解class和装饰器(下)

装饰器

在 vue 中,我们一般使用vue-class-component来把 vue 里面组件的写法转变为类形式的,写法如下:

<template>
  <div>{{ message }}</div>
</template>

<script type='ts'>
import { Vue, Component, Watch } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  // Declared as component data
  message = 'Hello World!'

  @Watch('visible')
  onVisibleChanged(newValue: any) {
    this.$emit('input', newValue);
  }
}
</script>

那么它是怎么实现的呢?主要分为 2 步:

  1. 在打包的时候会把装饰器打包成原始代码
  2. component 装饰器会对组件类做一些处理

装饰器是怎么打包的

装饰器是一个函数,它接收的三个参数:

  1. target(对象的 prototype,如果是类装饰器的话,就只有这一个参数)
  2. key(当前的方法名或属性名)
  3. descriptor(就是用于 defineProperty 的 config)

我们经常使用的 class 装饰器有 2 种(存在的一共有4种,我们只讨论常见的这 2 种):

  1. 放在 class 上的类装饰器
  2. 放在方法上的方法装饰器

首先我们来看一段示例代码:

function show(target, key, descriptor) {
  console.log(target);
  console.log(key);
  console.log(descriptor);
}

// 类装饰器
@show
class A {
  constructor(name) {
    this.name = name;
  }

  // 方法装饰器
  @show
  say() {
    console.log(this.name);
  }
}

打包之后的简化代码如下:

function _applyDecoratedDescriptor(target, property, decorators, descriptor) {
  var desc = {};

  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });

  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

  return desc;
}

function show(target, key, descriptor) {
  console.log(target);
  console.log(key);
  console.log(descriptor);
}

var A = show(_class = (_class2 = /*#__PURE__*/ function () {
  function A(name) {
    this.name = name;
  }

  A.prototype.say = function say() {
    console.log(this.name);
  }

  return A;
}(), (_applyDecoratedDescriptor(
  _class2.prototype,
  "say",
  [show],
  Object.getOwnPropertyDescriptor(_class2.prototype, "say")
  )
), _class2)) || _class;

可以看到:

  1. 对于类装饰器,只接收了 _class 参数,而_class =后面的括号里的三个值其实是一种顺序写法,最终返回的是括号里面的最后那个值也就是 _class2。
  2. 对于方法装饰器,会被放到一个数组里面去,然后调用 _applyDecoratedDescriptor 对被装饰的方法顺序执行各个装饰器(谁在上面谁先执行)。
  3. _applyDecoratedDescriptor 会收集 prototype、method key 和 property descriptor,然后传给装饰器进行执行。

到这里就很清晰了,装饰器其实并不是什么黑魔法,只是在编译的时候依次给类或者对象执行的函数罢了。

vue-class-component 是怎么实现 vue 的 class 写法的?

vue-class-component是通过 @component 装饰器来实现 vue 的 class 写法的,源码如下:

// index.ts
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  // 要装饰的类其实是函数类型,所以会从这里进入
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  // 如果传入一个对象的话,就返回一个装饰器函数
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

它对传入的参数做了一层适配:

  1. 如果传入的是函数类型,则证明是一个类,然后直接返回装饰器。(所以能够支持@component()这种不加参数的写法)
  2. 如果传入的不是函数类型,则证明是一个配置,然后返回装饰器函数。(所以能够支持@component(config)这种加参数的写法)

最终,它是通过调用 componentFactory 来进行装饰的,它的源码如下:

export const $internalHooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render',
  'errorCaptured', // 2.5
  'serverPrefetch' // 2.6
]

export function componentFactory (
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  options.name = options.name || (Component as any)._componentTag || (Component as any).name
  // prototype props.
  const proto = Component.prototype
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    if (key === 'constructor') {
      return
    }

    // 加上生命周期函数、钩子函数
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    if (descriptor.value !== void 0) {
      // 加上 methods
      if (typeof descriptor.value === 'function') {
        (options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // 使用 mixins 的形式加上 data
        (options.mixins || (options.mixins = [])).push({
          data (this: Vue) {
            return { [key]: descriptor.value }
          }
        })
      }
    } else if (descriptor.get || descriptor.set) {
      // 加上 computed
      (options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set
      }
    }
  })

  // 收集 constructor 上的 data
  ;(options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return collectDataFromConstructor(this, Component)
    }
  })

  // 处理其它装饰器(方法装饰器、属性装饰器等)
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }

  // 初始化 super 里面的实例属性
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
  const Extended = Super.extend(options)

  // 处理子类和父类的静态方法
  forwardStaticMembers(Extended, Component, Super)

  // 复制使用 reflect 声明的属性
  if (reflectionIsSupported()) {
    copyReflectionMetadata(Extended, Component)
  }

  return Extended
}

总的来说,这段代码做了如下工作。其实就是筛选出相应的属性和方法按 options 的形式进行组装罢了

  1. 保存一个内部钩子列表,筛选出生命周期钩子、路由钩子等作为相关的方法。(所以我们如果要加入路由钩子的话,首先需要先把它加到列表里面去)
  2. 筛选出 data、methods、computed 加到 options 上面去。(所以这些数据要遵循相应的写法)
  3. 收集 constructor 上的 data
  4. 初始化 super 里面的实例属性
  5. 处理子类和父类的静态方法
  6. 复制使用 reflect 声明的属性

这里需要注意的是,在收集 data 的时候,并不是直接把 data 进行赋值的,因为 data 可以是一个函数,所以这里使用mixins的方法进行混合。

vue-property-decorator 的 watch 装饰器的原理

vue-property-decorator是基于vue-class-component封装的库,它提供了很多方便的装饰器,现在我们来看下它的 watch 装饰器。源码如下:

// vue-class-component 库
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    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))
  }
}

// vue-property-decorator 库
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 })
  })
}

这段代码其实就是在 componentOptions 上面开了一个 watch 属性,用来把各个字符串的 watch 函数推进去。值得注意的是执行过程

  1. 首先执行方法装饰器,把装饰器工厂函数推到__decorators__保存起来,此时装饰器并没有被执行
  2. 然后执行 component 装饰器,在执行过程中,会把__decorators__里面的装饰器取出,然后执行,这个时候方法装饰器才生效了。

有一点非常奇怪,因为在 component 装饰器里面,会先把实例方法(就是 watch 的方法)挂载到 methods 里面去,然后再执行方法装饰器,把方法作为 handler 推到相应的 watch 数组里面去。那么这个实例方法不是没有从 methods 里面删除吗?看了半天源码也没找到删除的地方,期待大佬解答~~

posted @ 2020-10-26 11:06  馒头加梨子  阅读(898)  评论(0编辑  收藏  举报