本文只浅析@Component类装饰器的核心源码,其他原理相似,暂不赘述。

关于JS装饰器可查看本人另一篇:JS Decorator —— 装饰器(装饰模式)
关于@Prop属性装饰器、@Watch方法装饰器及vue-property-decorator源码可查看本人另一篇:源码探秘之 vue-property-decorator

先看一个使用 vue+ts 开发的一个组件例子:

<template>
</template><script lang="ts">
// 此处是从vue-property-decorator 统一引入的,实际 Component 定义在 vue-class-component 包内
import { Component, Vue, Prop, Watch } from 'vue-property-decorator';@Component({name: 'test' // 组件name
})
export default class Test extends Vue {// 父组件传递的参数@Prop({type: String, default: ''}) msg!:string// 定义的变量title = '标题'// 计算属性get computedMsg () {return 'computed ' + this.msg}// 侦听器@Watch('title', { immediate: true })watchTitleChange (val: string) {console.log('title change:', val);}// 方法initList () {// do something}// 生命周期mounted () {this.initList()}
}
</script>

经过@Component装饰器转换后,会变成标准的 Vue(2.x) 语法结构的子类(当然很多属性是通过mixin加入进来的):

<script>
export default {name: 'test',props: {msg: { type: String, default: '' }},data() {return {title: '标题',};},computed: {computedMsg() {return 'computed ' + this.msg},},watch: {title: {handle(val) {console.log('title change:', val);},immediate: true},},methods: {initList() {// do something},},mounted() {this.initList();},
};
</script>

源码开始

文件:/src/index.ts

// 为了兼容两种写法,Component实际上是既作为工厂函数,又作为装饰器函数
// 写法一,不带任何参数,则 option 为 Test 类的构造函数,即:typeof options === ’function‘
// @Component
// export default class Test extends Vue {
// 写法二,带参数,则 option 为传入的参数,则需要工厂函数返回一个装饰器,此时:typeof options === ’object‘
// @Component({ name: 'test' })
// export default class Test extends Vue {
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {// 写法一会调用的(不带参数)if (typeof options === 'function') {return componentFactory(options)}// 写法二会调用的(带参数)return function (Component: VueClass<Vue>) {return componentFactory(Component, options)}
}
export default Component

文件:/src/component.ts

// 第一,将传入的**构造函数原型**上的属性放入data中,将方法根据是否是生命周期钩子、是否是计算属性,来分别放入对应的位置。
// 第二,**实例化**构造函数,将构造函数实例化对象的**属性**放入data,实例化对象本身(不算原型上的)是不带有方法的,即使某个属性的值是function类型,也应该作为data来处理。
// 第三、对**构造函数自身的 静态 属性和方法**处理,处理方式同原型的处理方式。
// 第四,提供属性装饰器的拓展功能,Component只装饰了类,如果想对类中的属性做进一步的处理,可以从此入手,比如vue-property-decorator库提供的那些装饰器就是依赖这个拓展功能。
export function componentFactory (Component: VueClass<Vue>,options: ComponentOptions<Vue> = {}
): VueClass<Vue> {// 首先给 options.name 赋值,确保最终生成的对象具有 name 属性options.name = options.name || (Component as any)._componentTag || (Component as any).name// 获取构造函数原型,这个原型上挂在了该类的methodconst proto = Component.prototype// 遍历原型Object.getOwnPropertyNames(proto).forEach(function (key) {// 如果是constructor,则不处理。// 这也是为什么vue单文件组件类不需要constructor的直接原因,因为有也不会做任何处理if (key === 'constructor') {return}// 如果原型属性(方法)名是vue生命周期钩子名,则直接作为钩子函数挂载在options最外层if ($internalHooks.indexOf(key) > -1) {options[key] = proto[key]return}// 先获取到原型属性的descriptor。// 计算属性其实也是挂载在原型上的,所以需要对descriptor进行判断const descriptor = Object.getOwnPropertyDescriptor(proto, key)!// void 0 === undefinedif (descriptor.value !== void 0) {// 如果属性值是一个function,则认为这是一个方法,挂载在methods下if (typeof descriptor.value === 'function') {(options.methods || (options.methods = {}))[key] = descriptor.value} else {// 如果不是,则认为是一个普通的data属性。// 但是这是原型上,所以更类似mixins,因此挂在mixins下。// 一般写在类中的只有是函数才能放在原型上,但有别的方式可以把非函数的值添加到原型上:// 第一种,直接给原型添加属性// Home.prototype.age = 18;// 第二种,用属性装饰器// function ageDecorator(prototype, key){//   return {  // 装饰器返回描述对象,会在 prototype增加key这个属性//     enumerable: false,//     value: 18//   }// }// class Home extends Vue {//   @ageDecorator//   age: number = 18;// }(options.mixins || (options.mixins = [])).push({data (this: Vue) {return { [key]: descriptor.value }}})}} else if (descriptor.get || descriptor.set) {// 如果value是undefined(ps:void 0 === undefined)。// 且描述符具有get或者set方法,则认为是计算属性。(options.computed || (options.computed = {}))[key] = {get: descriptor.get,set: descriptor.set}}})// 收集构造函数实例化对象的属性作为data,并放入mixins// 这里再次添加了一个mixin,会把这个类实例化,然后把对象中的值放到mixin中;(options.mixins || (options.mixins = [])).push({data (this: Vue) {// 实例化Component构造函数,并收集其自身的(非原型上的)属性导出,内部还针对不同vue版本做了兼容。return collectDataFromConstructor(this, Component)}})// 处理属性装饰器,vue-class-component只提供了类装饰器。// 像props、components等特殊参数只能写在Component(options)的options参数里。// 通过这个接口可以扩展出属性装饰器,像vue-property-decorator库那种的属性装饰器const decorators = (Component as DecoratedClass).__decorators__if (decorators) {decorators.forEach(fn => fn(options))delete (Component as DecoratedClass).__decorators__}// 获取Vue对象// 得到继承的父类,不出意外为 Vue const superProto = Object.getPrototypeOf(Component.prototype)// 如果原型链上确实有 Vue,则得到构造函数;不为 Vue,则直接使用 Vue;// 目的是为了找到 extend 函数。const Super = superProto instanceof Vue? superProto.constructor as VueClass<Vue>: Vue// 通过vue.extend生成一个vue子类const Extended = Super.extend(options)// 在前面只处理了Component构造函数原型和其实例化对象的属性和方法。// 对于构造函数本身的静态属性还没有处理,在此处理,处理过程类似前面,不赘述。// 传入 生成的 Vue 实例,构造函数,Vue对象forwardStaticMembers(Extended, Component, Super)// 反射相关处理,这个是新特性,本人了解也不多,但到此已经不影响理解了,所以可以略过。// 如有对此了解的,欢迎补充。if (reflectionIsSupported()) {copyReflectionMetadata(Extended, Component)}// 最终返回这个vue实例对象return Extended
}export const $internalHooks = ['data','beforeCreate','created','beforeMount','mounted','beforeDestroy','destroyed','beforeUpdate','updated','activated','deactivated','render','errorCaptured', // 2.5'serverPrefetch' // 2.6
]// 传入 实例化的 Vue 实例,构造函数,Vue对象
function forwardStaticMembers (Extended: typeof Vue,Original: typeof Vue,Super: typeof Vue
): void {// We have to use getOwnPropertyNames since Babel registers methods as non-enumerableObject.getOwnPropertyNames(Original).forEach(key => {// Skip the properties that should not be overwrittenif (shouldIgnore[key]) {return}// Some browsers does not allow reconfigure built-in propertiesconst 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 propertiesif (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)})
}

文件:/src/data.ts

export function collectDataFromConstructor (vm: Vue, Component: VueClass<Vue>) {// override _init to prevent to init as Vue instance// 先保存原有的 _init,目的是不执行 Vue上的 _init 做其他初始化动作const originalInit = Component.prototype._init// 在被装饰的类的原型上手动增加 _init,在Vue实例化时内部会调用Component.prototype._init = function (this: Vue) {// proxy to actual vm// 拿到渲染组件对象上的属性,包括不可枚举的属性,包含组件内定义的 $开头属性 和 _开头属性,还有自定义的一些方法const keys = Object.getOwnPropertyNames(vm)// 2.2.0 compat (props are no longer exposed as self properties)// 如果渲染组件含有,props,但是并没有放在原组件实例上,则添加上if (vm.$options.props) {for (const key in vm.$options.props) {if (!vm.hasOwnProperty(key)) {keys.push(key)}}}// 把给原组件实例上 Vue 内置属性设置为不可遍历。keys.forEach(key => {Object.defineProperty(this, key, {get: () => vm[key],set: value => { vm[key] = value },configurable: true})})}// should be acquired class property values// 手动初始化要包装的类,目的是拿到初始化后实例const data = new Component()// restore original _init to avoid memory leak (#209)// 重新还原回原来的 _init,防止一直引用原有的实例,造成内存泄漏Component.prototype._init = originalInit// create plain data object// 重新定义对象const plainData = {}// Object.keys 拿到可被枚举的属性,添加到对象中Object.keys(data).forEach(key => {if (data[key] !== undefined) {plainData[key] = data[key]}})if (process.env.NODE_ENV !== 'production') {if (!(Component.prototype instanceof Vue) && Object.keys(plainData).length > 0) {warn('Component class must inherit Vue or its descendant class ' +'when class property is used.')}}return plainData
}

总结

  • 第一,将传入的构造函数原型上的属性放入data(mixin)中,将方法根据是否是生命周期钩子、是否是计算属性,来分别放入对应的位置。
  • 第二,将构造函数实例化,并把构造函数实例化对象的属性放入data(mixin),实例化对象本身(不算原型上的)是不带有方法的,即使某个属性的值是function类型,也应该作为data来处理。
  • 第三、对构造函数自身的 静态 属性和方法处理,直接使用Object.defineProperty 挂载到Vue实例上。
  • 第四,提供属性装饰器的拓展功能,Component只装饰了类,如果想对类中的属性做进一步的处理,可以从此入手,比如vue-property-decorator库提供的那些装饰器就是依赖这个拓展功能。

这里想要理解透彻,需要理解 构造函数原型上的属性、构造函数实例化对象的属性、构造函数自身的静态属性和方法,三者的区别,可以见得优秀的框架考虑的是很全面的。


码字不易,觉得有帮助的小伙伴点个赞支持下~


扫描上方二维码关注我的订阅号~

源码探秘之 vue-class-component相关推荐

  1. 源码探秘之 vue-property-decorator

    本文只浅析@Prop属性装饰器和@Watch方法装饰器的核心源码,其他原理相似,暂不赘述. 关于JS装饰器可查看本人另一篇:JS Decorator -- 装饰器(装饰模式) 关于@Component ...

  2. c++ gdb 绑定源码_【Vue原理】VNode 源码版

    ↑点击上方 "神仙朱" 一起研究Vue源码吧 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧 研究基于 Vue版 ...

  3. 可视化工具gephi源码探秘(二)---导入netbeans

    在上篇<可视化工具gephi源码探秘(一)>中主要介绍了如何将gephi的源码导入myeclipse中遇到的一些问题,此篇接着上篇而来,主要讲解当下通过myeclipse导入gephi源码 ...

  4. epoll源码探秘(epoll_create)

    epoll源码探秘(epoll_create) epoll系列的系统函数,很简单,但是很强大.epoll_create(),epoll_ctl() , epoll_wait(),三个就够了. 一些重要 ...

  5. 【protobuf源码探秘】编码、序列化

    文章目录 为什么要写这篇? 编码 编码结构 Varints 编码 负数的 Varints 编码情况 ZigZag 编码 bool fixed族 不定长数据类型 repeat repeated stri ...

  6. StringRedisTemplate与RedisTemplate异同源码探秘

    StringRedisTemplate与RedisTemplate异同源码探秘 StringRedisTemplate与RedisTemplate异同 1.两者的关系是StringRedisTempl ...

  7. 【Mybatis+spring整合源码探秘】--- mybatis整合spring事务原理

    文章目录 1 mybatis整合spring事务原理 1 mybatis整合spring事务原理 本篇文章不再对源码进行具体的解读了,仅仅做了下面一张图: 该图整理了spring+mybatis整合后 ...

  8. Soul网关源码探秘《十》 - 负载均衡 - Hash/RoundRobin

    前文分析了DividePlugin插件中负载均衡的总体结构以及Random的具体实现.今天来探索剩下两种负载均衡是如何实现的. 准备工作 见Soul网关源码探秘<八> - 负载均衡初探 H ...

  9. 千层套路 - Vue 3.0 初始化源码探秘

    关注若川视野, 回复"pdf" 领取资料,回复"1",可加群长期交流学习 刘崇桢,微医云服务团队前端工程师,左手抱娃.右手持家的非典型码农. 9 月初 Vue. ...

最新文章

  1. innodb表 手工导入导出
  2. 谷歌相册也不能无限白嫖了,「地主家」也烧不起免费网盘
  3. 【Android 内存优化】Bitmap 长图加载 ( BitmapRegionDecoder 简介 | BitmapRegionDecoder 使用流程 | 区域解码加载示例 )
  4. java多线程操作同一资源
  5. 【David Silver强化学习公开课】-5:Model-Free Control
  6. 一些 Google 搜索词
  7. 领域研究热点的绝妙探索方法
  8. linux ppc64 是什么,docker - 在(模拟)PPC64 Linux上的backtrace()segfaults - 堆栈内存溢出...
  9. 均线策略---使用quartz实现策略
  10. ​全网首发,TensorFlow 2.0 中文视频教程来啦
  11. 解决tab切换的时候,swiper不生效
  12. Linux下apache+php+mysql配置攻略
  13. 第三节基础篇—SQL的约束
  14. 一个小小的签到功能,到底用MySQL还是Redis?
  15. C#进阶系列——WebApi 路由机制剖析:你准备好了吗?
  16. Windows上搭建Git服务器
  17. 三菱plc软件测试程序com端口,三菱plc编程软件com端口不能保存
  18. 【流水账】对Pupper的软件设备进行配置(树莓派)
  19. 微信小程序人脸识别方案
  20. 【读书笔记】淘宝技术这十年

热门文章

  1. mysql 选择前五项数据_历史五项基础数据之最!都知道得分者是张伯伦,那其他的呢?...
  2. Google(谷歌)中国总部探秘[z]
  3. 北京地铁6号线部分图形
  4. Bootstrap 折叠(collapse)插件面板
  5. 全球“黑客大赛”冠军霸气讲述:我是如何让50个文件一起骗过AI安防系统的?...
  6. hl-3150cdn硒鼓安装_兄弟hl3150cdn墨盒怎么装
  7. Android高仿iOS Messages录音操作按钮
  8. c语言中swith的用法,语法复习十八:数 词
  9. 在JavaScript中实现队列
  10. python抢红包脚本实例-自动抢红包,点赞朋友圈,python解放你的双手