深入理解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 里面刪除嗎?看了半天源碼也沒找到刪除的地方,期待大佬解答~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM