• Vue-Property-Decorator源码分析


    概述

    vue-property-decorator是基于vue组织里vue-class-component所做的拓展,先来了解一下vue-class-component

    Vue-Class-Component

    vue-class-component是一个Class Decorator,也就是类的装饰器,但目前装饰器在vue中只属于草案阶段.

    原理简述

    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. 反射相关处理

    相关文章:

    https://zhuanlan.zhihu.com/p/48371638

    http://www.lutoyvan.cn/2019/03/05/vue-class-component-source-code-analysis.html

    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里的参数

  • 相关阅读:
    非嵌入式数据库 软件很难普及 玩大
    FireMonkey下的异形窗体拖动(需要使用FmxHandleToHwnd函数转化一下句柄)
    公司开到高新技术区可以只收11的企业所得税,拿到软件企业认证后可享受所得税两免三减半(从盈利年度算起)
    猜测:信号槽的本质是使用Windows的自定义消息来实现的
    服务运行、停止流程浅析
    在线压缩JS的工具
    通用流程设计
    并行Linq(一)
    SQL基础知识总结(一)
    Easyui布局
  • 原文地址:https://www.cnblogs.com/scottjeremy/p/11991030.html
Copyright © 2020-2023  润新知