Vue-Property-Decorator原始碼分析

Scott Jeremy發表於2019-08-20

概述

vue-property-decorator是基於vue組織裡vue-class-component所做的擴充,先來了解一下vue-class-component

Vue-Class-Component

vue-class-component是一個Class Decorator,也就是類的裝飾器.

原理簡述

vue2.x只有Object一種宣告元件的方式, 比如這樣:

const App = Vue.extend({
    // data
    data() {
        return {
            hello: 'world',
        };
    },
    computed: {
        world() {
            return this.hello + 'world';
        },
    },
    // hooks
    mounted() {
        this.sayHello();
    },
    // methods
    methods: {
        sayHello() {
            console.log(this.hello);
        },
    },
});
複製程式碼

用了vue-class-component就成了以下寫法:

import Component from 'vue-class-component';

@Component({
    name: 'App'
})
class App extends Vue {
    hello = 'world';

    get world() {
        return this.hello + 'world';
    }

    mounted() {
        this.sayHello();
    }

    sayHello() {
        console.log(this.hello);
    }
}
複製程式碼

在這個例子中,很容易發現幾個疑點:

  1. @Component()是什麼?
  2. hello = 'world'這是什麼語法?
  3. App類沒有constructor建構函式;
  4. 匯出的類沒有被new就直接使用了;

疑點1:

對裝飾器的有一定了解. 裝飾器種類有好幾種, vue-class-component中主要使用了類裝飾器. 更多關於裝飾器資訊請參閱阮老師的文章: ECMAScript6入門

看完阮老師所寫的文章已經可以解決了疑點1

簡述: @Component就是一個修飾器, 用來修改類的行為

疑點2:

在JS語法中, class中都是需要在constructor中給屬性賦值, 在chrome上像vue-class-component中定義class是會報錯的, 但vue-class-component中卻又這麼做了.

然後我們看看class通過webpack + babel-loader解析後會變成什麼樣子

// 轉換前
class App {
    hello = 'world';

    sayHello() {
        console.log(this.hello);
    }
}

// 轉換後
function App () {
    this.hello = 'world'
}

App.prototype.sayHello = function () {
    console.log(this.hello);
}
複製程式碼

接下來看看入口檔案index.ts所做的東西:

// Component實際上是既作為工廠函式,又作為裝飾器函式
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    // 區別一下。這裡的命名雖然是工廠,其實它才是真正封裝裝飾器邏輯的函式
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}
複製程式碼

再看看componentFactory所做的東西:

import Vue, { ComponentOptions } from 'vue'
import { copyReflectionMetadata, reflectionIsSupported } from './reflect'
import { VueClass, DecoratedClass } from './declarations'
import { collectDataFromConstructor } from './data'
import { hasProto, isPrimitive, warn } from './util'

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> {
  // 為component的name賦值
  options.name = options.name || (Component as any)._componentTag || (Component as any).name
  // prototype props.
  // 獲取原型
  const proto = Component.prototype
  // 遍歷原型
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    // 如果是constructor, 則不處理
    if (key === 'constructor') {
      return
    }

    // hooks
    // 如果原型屬性(方法)名是vue生命週期鉤子名,則直接作為鉤子函式掛載在options最外層
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }
    // getOwnPropertyDescriptor 返回描述物件
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    // void 0 === undefined
    if (descriptor.value !== void 0) {
      // methods
      // 如果是方法名就掛載到methods上
      if (typeof descriptor.value === 'function') {
        (options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // typescript decorated data
        // 把成員變數作為mixin放到options上,一個變數一個mixin,而不是直接統計好放到data或者同一個mixin中
        // 因為data我們已經作為了保留欄位,可以在類中宣告成員方法data()和options中宣告data同樣的方法宣告變數
        (options.mixins || (options.mixins = [])).push({
          data (this: Vue) {
            return { [key]: descriptor.value }
          }
        })
      }
    } else if (descriptor.get || descriptor.set) {
      // computed properties
      // 轉換成計算屬性的getter和setter
      (options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set
      }
    }
  })

  // add data hook to collect class properties as Vue instance's data
  // 這裡再次新增了一個mixin,會把這個類例項化,然後把物件中的值放到mixin中
  // 只有在這裡我們宣告的class的constructor被呼叫了
  ;(options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return collectDataFromConstructor(this, Component)
    }
  })

  // decorate options
  // 如果這個類還有其他的裝飾器,也逐個呼叫. vue-class-component只提供了類裝飾器
  // props、components、watch等特殊引數只能寫在Component(options)的options引數裡
  // 因此我們使用vue-property-decorator庫的屬性裝飾器
  // 通過下面這個迴圈應用屬性裝飾器就可以合併options(ps: 不明白可以看看createDecorator這個函式)
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }

  // find super
  // 找到這個類的父類,如果父類已經是繼承於Vue的,就直接呼叫它的extend方法,否則呼叫Vue.extend
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
  // 最後生成我們要的Vue元件
  const Extended = Super.extend(options)

  // 處理靜態成員
  forwardStaticMembers(Extended, Component, Super)

  // 如果我們支援反射,那麼也把對應的反射收集的內容繫結到Extended上
  if (reflectionIsSupported) {
    copyReflectionMetadata(Extended, Component)
  }

  return Extended
}

const reservedPropertyNames = [
  // Unique id
  'cid',

  // Super Vue constructor
  'super',

  // Component options that will be used by the component
  'options',
  'superOptions',
  'extendOptions',
  'sealedOptions',

  // Private assets
  'component',
  'directive',
  'filter'
]

const shouldIgnore = {
  prototype: true,
  arguments: true,
  callee: true,
  caller: true
}

function forwardStaticMembers (
  Extended: typeof Vue,
  Original: typeof Vue,
  Super: typeof Vue
): void {
  // We have to use getOwnPropertyNames since Babel registers methods as non-enumerable
  Object.getOwnPropertyNames(Original).forEach(key => {
    // Skip the properties that should not be overwritten
    if (shouldIgnore[key]) {
      return
    }

    // Some browsers does not allow reconfigure built-in properties
    const extendedDescriptor = Object.getOwnPropertyDescriptor(Extended, key)
    if (extendedDescriptor && !extendedDescriptor.configurable) {
      return
    }

    const descriptor = Object.getOwnPropertyDescriptor(Original, key)!

    // If the user agent does not support `__proto__` or its family (IE <= 10),
    // the sub class properties may be inherited properties from the super class in TypeScript.
    // We need to exclude such properties to prevent to overwrite
    // the component options object which stored on the extended constructor (See #192).
    // If the value is a referenced value (object or function),
    // we can check equality of them and exclude it if they have the same reference.
    // If it is a primitive value, it will be forwarded for safety.
    if (!hasProto) {
      // Only `cid` is explicitly exluded from property forwarding
      // because we cannot detect whether it is a inherited property or not
      // on the no `__proto__` environment even though the property is reserved.
      if (key === 'cid') {
        return
      }

      const superDescriptor = Object.getOwnPropertyDescriptor(Super, key)

      if (
        !isPrimitive(descriptor.value) &&
        superDescriptor &&
        superDescriptor.value === descriptor.value
      ) {
        return
      }
    }

    // Warn if the users manually declare reserved properties
    if (
      process.env.NODE_ENV !== 'production' &&
      reservedPropertyNames.indexOf(key) >= 0
    ) {
      warn(
        `Static property name '${key}' declared on class '${Original.name}' ` +
        'conflicts with reserved property name of Vue internal. ' +
        'It may cause unexpected behavior of the component. Consider renaming the property.'
      )
    }

    Object.defineProperty(Extended, key, descriptor)
  })
}
複製程式碼

下面簡單總結一下vue-class-component做了什麼:

  1. 收集class中的屬性, 如果是方法就放到Methods裡, 如果是普通變數就放到mixin中的data裡
  2. 例項化class, 把這個class的屬性也作為mixin中的data, 我們所寫class的建構函式只會被這裡所呼叫
  3. 利用Options執行生成元件
  4. 處理靜態屬性
  5. 反射相關處理

相關文章:

zhuanlan.zhihu.com/p/48371638

www.lutoyvan.cn/2019/03/05/…

vue-property-decorator

vue-property-decorator是在vue-class-component基礎上新增了幾個屬性裝飾器

這裡採用幾種常用方式做介紹

Prop:

interface PropOptions<T=any> {
  type?: PropType<T>;
  required?: boolean;
  default?: T | null | undefined | (() => T | null | undefined);
  validator?(value: T): boolean;
}

export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
  return (target: Vue, key: string) => {
    applyMetadata(options, target, key)
    // 把props push到vue-class-component的__decorators__陣列中
    createDecorator((componentOptions, k) => {
      ;(componentOptions.props || ((componentOptions.props = {}) as any))[
        k
      ] = options
    })(target, key)
  }
}

/** @see {@link https://github.com/vuejs/vue-class-component/blob/master/src/reflect.ts} */
const reflectMetadataIsSupported =
  typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined'

// 設定型別
function applyMetadata(
  options: PropOptions | Constructor[] | Constructor,
  target: Vue,
  key: string,
) {
  if (reflectMetadataIsSupported) {
    if (
      !Array.isArray(options) &&
      typeof options !== 'function' &&
      typeof options.type === 'undefined'
    ) {
      // 型別後設資料使用後設資料鍵"design:type"
      // 參考文章:https://www.jianshu.com/p/2abb2469bcbb
      options.type = Reflect.getMetadata('design:type', target, key)
    }
  }
}

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))
  }
}
複製程式碼

Watch:

/**
 * 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 })
  })
}
複製程式碼

綜上所述, 其實和 vue-class-component 一個原理 都是用裝飾器去解析出適用於vue裡的引數

相關文章