vue2class写法源码分析vue-class-component(1)

1,244 阅读5分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

1. 前言

传统的vue写法组件其实就是一个js对象, 里面不能很好地配合编辑器做类型推断, 特别是this的指向总是any, 所以Vue的作者尤雨溪大佬提供了这种class的写法, 以提供更好的类型推断和新的组件写法

这里先分析vue-class-component, 后面会再写一篇分析vue-property-decorator

这里的使用方法都是class的装饰器写法, 所以不了解的可以去看看阮一峰《ECMAScript 6 入门》装饰器

1.1 前置知识

1.1.1 类的语法

class Component {
  // 1. 实例属性新写法
  msg = '消息'
  constructor() {
    // 2. 实例属性的传统写法
    this.otherMsg = '另一条消息'
    this._count = 0
  }
  // 3. 原型链方法
  add() {
    this._count++
  }
  // 4. 计算属性的写法
  get count() {
    return this._count
  }
  set count(count) {
    this._count = count
  }
}

1.1.2 装饰器语法

// 1. 装饰器可以装饰类和方法(包含属性, 如果装饰属性, 会同时增加一个同名原型方法),都是函数, 并且是在编译时(肯定是在类被用到之前)执行的, 先执行方法装饰器, 再执行类装饰器
/**
 * 装饰类, 只接收一个参数
 * @param classFn 它装饰的类
 * @returns 装饰过的类
 */
const ClassDecorator = (classFn: any) => {
  return classFn
}
/**
 * 装饰方法, 接收三个参数,
 * @param target 类的原型对象
 * @param name 要装饰的属性名
 * @param descriptor 要装饰的属性名的描述对象
 * @returns 装饰过的属性的描述符
 */
const MethodDecorator = (target: any, name: string, descriptor?: any) => {
  console.log(target, name, descriptor)
  return descriptor
}
@ClassDecorator
class Component {
  @MethodDecorator
  method() {}
  @MethodDecorator
  a: any
  @MethodDecorator
  get b() {
    return 0
  }
}
export { Component }

// 2. 同一个类或者同一个方法可以同时被多个装饰器装饰, 会从下到上倒着装饰

const MultipleClassDecorator1 = (classFn: any) => {
  console.log(`我是 MultipleClassDecorator1, 我被调用了`)

  return classFn
}
const MultipleClassDecorator2 = (...args: any[]) => {
  console.log(
    `我是为了获取装饰器函数MultipleClassDecorator2时被调用的, 接收到参数`,
    args
  )

  return (classFn: any) => {
    console.log(`我是 MultipleClassDecorator2, 我被调用了`)
    return classFn
  }
}
const MultipleClassDecorator3 = (classFn: any) => {
  console.log(`我是 MultipleClassDecorator3, 我被调用了`)
  return classFn
}
const MultipleClassDecorator4 = (...args: any[]) => {
  console.log(
    `我是为了获取装饰器函数MultipleClassDecorator4时被调用的, 接收到参数`,
    args
  )

  return (classFn: any) => {
    console.log(`我是 MultipleClassDecorator4, 我被调用了`)
    return classFn
  }
}

@MultipleClassDecorator1
@MultipleClassDecorator2('我是最先执行的') // 这个是为了获取装饰器函数的调用, 最先调用, 并不是去装饰装饰器
@MultipleClassDecorator3
@MultipleClassDecorator4('我稍晚执行', '我传俩参数')
class MultipleClass {}

export { Component, MultipleClass }

上面的打印 image.png

好了, 有了以上知识, 开始下面的分析

2. 用法

有两种用法(传参与不传参), 都必须用@Component装饰一下, 不然导出的类不是组件对象, 记得那时候, 我遇到这个问题, 还找了一段时间的bug...

// 用法一, 直接装饰类, 没有传入参数
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class App extends Vue {}
// 用法二, 传入了参数, 参数可以是undefined, 也可以是一个options, 别传函数就行
import { Component, Vue } from 'vue-property-decorator'
@Component({
  methods: {
    myHandler() {},
  },
})
export default class App extends Vue {}

3. 源码

首先这里需要注意的一点是, 在类的装饰器里, 是拿不到实例上的属性的, 需要构造一下之后, 才能拿到实例上的属性

3.1 导出了一个全局的函数

function Component(options: ComponentOptions<Vue> | VueClass<Vue>): any {
  // 1. 用法一, 它是作为装饰器使用的,
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  // 2. 用法二, 作为获取装饰器的函数被调用, 下面的才是装饰器函数
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

export default Component

3.2 组件工厂函数

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选项
  // 这个类装饰器函数, 功能就是从类中拿到分散的 methods computed 和 data 以及 一些生命周期 合并到 传入的 options 上, 然后通过 Vue.extend(options) 返回一个组件
  options.name =
    options.name || (Component as any)._componentTag || (Component as any).name
  // 从原型上拿到 methods 和 computed 还有很少会在原型上出现的 data
  const proto = Component.prototype
  // 只读取 当前类原型的自有属性, 没有读取到它继承的类的原型上的属性
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    if (key === 'constructor') {
      return
    }

    // options 上原有的方法, 大多都是些生命周期, 直接赋值到 options 上
    if ($internalHooks.includes(key)) {
      options[key] = proto[key]
      return
    }
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    // 如果方法装饰器返回不正确的描述对象, 它的值就是undefined
    if (descriptor.value !== void 0) {
      if (typeof descriptor.value === 'function') {
        // 原型上的函数
        // 这里直接对传入的 options.methods 进行赋值, 所以可能会覆盖掉传入的 options.methods 上原本的值
        ;(options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // 不是函数的, 都认为是 data, 通过 mixins 混入
        ;(options.mixins || (options.mixins = [])).push({
          data(this: Vue) {
            return { [key]: descriptor.value }
          },
        })
      }
    } else if (descriptor.get || descriptor.set) {
      // 类上的计算属性
      ;(options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set,
      }
    }
  })
  ;(options.mixins || (options.mixins = [])).push({
    data(this: Vue) {
      // 这里是收集类上的实例属性, 因为在 原型上读取不到 实例属性, 需要构造一下才可以拿到
      // 它的调用时机是, 组件被实例化时, 获取 data 的时候 initState => initData
      // 第一个参数 this, 指向的是 这个组件
      // 不要跟进去看了, 只需要知道, 在这里拿到类上的实例属性就好了, 好累....
      return collectDataFromConstructor(this, Component)
    },
  })

  // 这里给方法装饰器留了个口子, vue-property-decorator 会用到, 因为方法装饰器先调用, 所以在方法装饰器中, 把预先的回调放入 __decorators__ 中, 在这里统一回调
  // 之所以这样做, 是因为在方法装饰器中拿不到 options, 这里可以给它, 让它处理一下
  // 这里在下一篇, 分析 vue-property-decorator 时, 还会提到
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    decorators.forEach((fn) => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }

  // 下面就是将 合并生成的 options 通过 Vue.extend(options) 并返回
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super =
    superProto instanceof Vue ? (superProto.constructor as VueClass<Vue>) : Vue
  // 在Super.extend里面会将mixins合并到Extended.options上
  const Extended = Super.extend(options)
  // 将 Component 上的静态属性 搬运到 Extended 上
  // forwardStaticMembers(Extended, Component, Super)
  // 也是搬运一些东西
  // if (reflectionIsSupported()) {
  //   copyReflectionMetadata(Extended, Component)
  // }

  return Extended
}
export function collectDataFromConstructor(vm: Vue, Component: VueClass<Vue>) {
  // 保存 _init 方法, 因为想要拿到类上的实例属性需要 构造一下( new 一下), 构造时只会调 _init 方法
  const originalInit = Component.prototype._init
  // 这里重写 _init 方法, 除了上面的作用, 还有一个作用是, 将在vm.$options.props 但是不在 vm 上的属性 赋值成实例属性
  // 如果vm.$options.props 是undefined, 可以不看下面了
  Component.prototype._init = function (this: Vue) {
    const keys = Object.getOwnPropertyNames(vm)
    if (vm.$options.props) {
      for (const key in vm.$options.props) {
        if (!vm.hasOwnProperty(key)) {
          keys.push(key)
        }
      }
    }
    // keys 得到的是在 vm.$options.props 但是不在 vm 上的属性
    keys.forEach((key) => {
      // 这里的 this 指向是 Component 实例, 最后是下面 new 出来的 data
      // vm 是 组件
      Object.defineProperty(this, key, {
        get: () => vm[key],
        set: (value) => {
          vm[key] = value
        },
        configurable: true,
      })
    })
  }
  // 得到实例上的属性
  const data = new Component()

  // 恢复原型
  Component.prototype._init = originalInit

  // create plain data object
  const plainData = {}
  Object.keys(data).forEach((key) => {
    if (data[key] !== undefined) {
      plainData[key] = data[key]
    }
  })

  return plainData
}

4. 总结

这个类装饰器函数, 功能就是从类中拿到分散的 methods computeddata 以及 一些生命周期 合并到 传入的 options 上, 然后通过 Vue.extend(options) 返回一个组件

$internalHooks 中的会直接覆盖赋值传入的options, 这其中包括data, 其它都是些生命周期函数

类型拿到位置合并方式
function原型链上覆盖赋值到options.methods
get & set原型链上覆盖赋值到options.computed
属性原型链上&实例属性上mixindata

需要注意这些覆盖赋值

5. 最后

后面准备再写一篇vue-property-decorator, 然后就不再写vue2的了, 直接开始vue3 按照惯例, 附上之前写的几篇文章

  1. vue2 源码解析之 nextTick
  2. 代码片段之 js 限流调度器
  3. 数据结构与算法之链表(1)
  4. vue2 源码解析之事件系统$on
  5. vue2-全局 api 源码分析