深入理解vue-class-component源码阅读


Posted in Javascript onFebruary 18, 2019

vue-class-component是vue作者尤大推出的一个支持使用class方式来开发vue单文件组件的库。但是,在使用过程中我却发现了几个奇怪的地方。

首先,我们看一个简单的使用例子:

// App.vue
<script>
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
  props: {
    propMessage: String
  }
})
export default class App extends Vue {
  // initial data
  msg=123

  // use prop values for initial data
  helloMsg='Hello, '+this.propMessage

  // lifecycle hook
  mounted () {
    this.greet()
  }

  // computed
  get computedMsg () {
    return'computed '+this.msg
  }

  // method
  greet () {
    alert('greeting: '+this.msg)
  }
}
</script>

//main.js
import App from './App.vue'

newVue({
  el: '#app',
  router,
  store,
  components: {
    App
  },
  template: '<App/>'
})

在这个例子中,很容易发现几个疑点:

1. App类居然没有constructor构造函数;
2. 导出的类居然没有被new就直接使用了。
3. msg=123,这是什么语法?

首先,针对前两个疑问,需要说明一下,class不一定非得有构造函数,同样也不一定非得使用new才能使用。熟悉原理的朋友应该知道,class只是一个ES6的语法糖,说白了还是一个Function而已。但是,这两点无疑是class这个语法糖的重要价值所在,可这里却偏偏没用,不由让人奇怪,甚至会想,既然不当class用,那为什么不干脆就用Function呢?

而第三点,却是妥妥点的语法错误啊,为此我还特意打开了Chrome控制台试验了一下,确实报错了。实验结果如下:

深入理解vue-class-component源码阅读

那这到底是怎么回事呢?出于程序员的好奇心,我对vue-class-component的源码探索了一番。下面就一起来看看,相信看完就可以解答上面的疑惑了。

第一步,在看源码之前,必须对装饰器的知识有一定了解。装饰器种类有好几种,vue-class-component中主要用了类装饰器,本文只对类装饰器做简单介绍,更多信息请参阅阮老师的文章:ECMAScript 6入门。

类装饰器,顾名思义,就是用来装饰一个类的,说的直白点就是用于修改一个类的。它具体有两种用法。如下:

// 用法一
function Decorator (target) {  
  // 处理target  
  return target
}

@Decorator
class ClassTest () {}

// 用法二
function DecoratorFactory (options) {  
  return function Decorator (target) {    
    //@todo 利用options一起处理target     
    // 然后返回 
    return target  
  }
}

@DecoratorFctory(options)
class ClassTest () {}

在两个用法中,我们将Decorator称为装饰器函数,DecoratorFactory称为装饰器工厂。

类装饰器函数规定只能接收类构造函数本身,如果还需要额外的参数传入,则需要使用装饰器工厂函数。

我们以装饰器工厂函数为例,说明其执行流程:

1. JS引擎首先会执行工厂函数,然后保存其返回的装饰器函数;
2. 然后解析class,将其转化为一个构造函数;
3. 将上述构造函数作为参数执行第一步得到的装饰器函数。
4. 如果装饰器函数有返回值,则会将类变量(如例子中的ClassTest变量)指向返回值,否则类变量仍然指向构造函数,基于JS引用变量的特点,即使仍指向原构造函数,这个构造函数也可能在装饰器中被改造过了。

直接使用装饰器函数的情况类似上面,只是少了装饰器工厂这一步处理过程。

了解了基本知识,我们开始第二步,解析vue-class-component执行流程。这里将根据装饰器的执行流程,分三个部分讲解。第一,工厂函数做了什么;第二,class解析之后是什么样的;第三,装饰器函数又做了什么。

工厂函数做了什么?

// vue-class-component使用的是TS语法
// Component实际上是既作为工厂函数,又作为装饰器函数
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeofoptions==='function') {
    // 区别一下。这里的命名虽然是工厂,其实它才是真正封装装饰器逻辑的函数
    return componentFactory (options)
  }
  return function (Component:VueClass<Vue>){
    return componentFactory(Component,options)
  }
}

从源码中可以看出,Component函数只是对参数进行了判断,说明它既可以用作工厂函数,也可以用作装饰器函数。而实际装饰器的逻辑则被封装在componentFactory函数里,这里对命名需要注意区分下,此工厂非彼工厂。

Class解析之后是什么样的

在文章开头我们就有疑问,在class中不经过constructor直接给其属性赋值是不符合JS语法的,而且我们还在Chrome上试验过了,确实会报错。但我们在使用component-class-component时却又实实在在那么干了,并且也没什么问题,这是怎么回事呢?
事实上,Chrome等主流浏览器对于ES6以及更高级的ES7、ES8的支持是不完整的,很多功能特性都不支持,这也是我们平时为什么都会使用babel来将高级的ES语法转换成ES5的原因。而我们前面提及的这点疑惑正是这个原因,Chrome不支持,不代表babel不支持。

不过,即便如此,我们又产生了一个新的疑惑,这种语法我没见过,那么经过babel转换后的class会是什么样的呢?毕竟这个转换结果会作为参数传递 给Component装饰器来处理,要想了解Component的处理过程,这个参数需要先了解。
于是,我在Component函数内添加了一条console.log(),得到了打印后的结果,只是我使用的webpack+babel-loader执行的编译,结果比较难以阅读,我简单翻译了一下,并和class源码一起对比如下:

// 转换前
class User {
  name = 'yl'
  age = 10

  get computeMethod () {
    cnsole.log(1)
  }

  method () {
    console.log(2)
  }
}

// 转换后
function User () {
  this.name = 'yl'
  this.age = 10
}

// 计算属性定义
User.prototype.defineProperty(this, 'computeValue', {
  get () {
    console.log(1)
    return this.name
  }
})

User.prototype.method = function () {
  console.log(2)
}

由此,我们也可以推测出,一个.vue文件导出的类会被解析成什么样子。

装饰器函数又做了什么

此时,我们已经知晓了传递给装饰器函数的参数是什么样了。这个参数应该是一个构造函数,它的主体会对类实例的属性进行赋值,它的原型则携带着各种属性和方法。
而我们知道的,如果不使用vue-class-component,那么一个.vue文件应该导出如下对象:

export default {
  name: 'test',
  data () {
    return {...}
  },
  computed: {
    com1 () {...},
    com2 () {...}
  },
  methods: {...},
  // 各种hook函数
}

很显然,装饰器函数必然是将传入的组件构造函数转换成了一个vue配置对象。那么,具体内部是怎么做的呢?我们来看看源码。(源码笔者加上了详细注释,但较长,可以直接跳过看后面的总结。)

// 这个函数就是封装了装饰器逻辑的函数,接受两个参数:
// 第一个是所装饰的类的构造函数;第二个是开发者传入的mixins对象
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
 // 获取构造函数原型,这个原型上挂在了该类的method
 const 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)!
  if (descriptor.value !== void 0) {
   // 如果属性值是一个function,则认为这是一个方法,挂载在methods下
   if (typeof descriptor.value === 'function') {
    (options.methods || (options.methods = {}))[key] = descriptor.value
   } else {
    // 如果不是,则认为是一个普通的data属性。
    // 但是这是原型上,所以更类似mixins,因此挂在mixins下。
    (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方法,则认为是计算属性。不理解的参考我上面关于class转换成构造函数的例子
   // 这里可能和普通的计算属性不太一样,因为一般计算属性只是用来获取值的,但这里却有setter。
   // 不过如果不使用setter,与非class方式开发无异,但有这一步处理,在某些场景会有特效。
   (options.computed || (options.computed = {}))[key] = {
    get: descriptor.get,
    set: descriptor.set
   }
  }
 })

 // 收集构造函数实例化对象的属性作为data,并放入mixins
 (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对象
 const superProto = Object.getPrototypeOf(Component.prototype)
 const Super = superProto instanceof Vue
  ? superProto.constructor as VueClass<Vue>
  : Vue
 // 通过vue.extend生成一个vue实例
 const Extended = Super.extend(options)

 // 在前面只处理了Component构造函数原型和其实例化对象的属性和方法。
 // 对于构造函数本身的静态属性还没有处理,在此处理,处理过程类似前面,不赘述。
 forwardStaticMembers(Extended, Component, Super)

 // 反射相关处理,这个是新特性,本人了解也不多,但到此已经不影响理解了,所以可以略过。
 // 如有对此了解的,欢迎补充。
 if (reflectionIsSupported) {
  copyReflectionMetadata(Extended, Component)
 }

 // 最终返回这个vue实例对象
 return Extended
}

源码较长,在此总结一下。这里主要做了四件事:

第一,将传入的构造函数原型上的属性放入data中,将方法根据是否是生命周期钩子、是否是计算属性,来分别放入对应的位置。

第二,实例化构造函数,将构造函数实例化对象的属性放入data,实例化对象本身(不算原型上的)是不带有方法的,即使某个属性的值是function类型,也应该作为data来处理。

第三、对构造函数自身的静态属性和方法处理,处理方式同原型的处理方式。

第四,提供属性装饰器的拓展功能,Component只装饰了类,如果想对类中的属性做进一步的处理,可以从此入手,比如vue-property-decorator库提供的那些装饰器就是依赖这个拓展功能。

说到此,想必大家对前面的疑惑也释然了,同时对vue-class-component的实现原理也有了一个大体的思路。因本人技术有限,文中可能存在肤浅、错误的地方,如有发现,还请不吝赐教,感谢!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
走出JavaScript初学困境—js初学
Dec 29 Javascript
javascript 学习之旅 (3)
Feb 05 Javascript
jQuery实现类似淘宝购物车全选状态示例
Jun 26 Javascript
Javascript之this关键字深入解析
Nov 12 Javascript
javascipt匹配单行和多行注释的正则表达式
Nov 20 Javascript
jquery css 设置table的奇偶行背景色示例
Jun 03 Javascript
Bootstrap表单Form全面解析
Jun 13 Javascript
用javascript获取任意颜色的更亮或更暗颜色值示例代码
Jul 21 Javascript
在Vuex使用dispatch和commit来调用mutations的区别详解
Sep 18 Javascript
Vue中Quill富文本编辑器的使用教程
Sep 21 Javascript
浅谈vue权限管理实现及流程
Apr 23 Javascript
js实现上传图片到服务器
Apr 11 Javascript
详解TypeScript+Vue 插件 vue-class-component的使用总结
Feb 18 #Javascript
jQuery实现的卷帘门滑入滑出效果【案例】
Feb 18 #jQuery
详解ES7 Decorator 入门解析
Feb 18 #Javascript
jQuery插件实现非常实用的tab栏切换功能【案例】
Feb 18 #jQuery
详解关于微信setData回调函数中的坑
Feb 18 #Javascript
jQuery实现的五星点评功能【案例】
Feb 18 #jQuery
JS中min函数实例讲解
Feb 18 #Javascript
You might like
深入浅出php socket编程
2015/05/13 PHP
WordPress中用于获取及自定义头像图片的PHP脚本详解
2015/12/17 PHP
php实现base64图片上传方式实例代码
2017/02/22 PHP
Laravel访问出错提示:`Warning: require(/vendor/autoload.php): failed to open stream: No such file or di解决方法
2019/04/02 PHP
Jquery判断$(&quot;#id&quot;)获取的对象是否存在的方法
2013/09/25 Javascript
JavaScript日期时间格式化函数分享
2014/05/05 Javascript
javascript用函数实现对象的方法
2015/05/14 Javascript
JavaScript中的Math.LOG2E属性使用详解
2015/06/14 Javascript
Laydate时间组件在火狐浏览器下有多时间输入框时只能给第一个输入框赋值的解决方法
2016/08/18 Javascript
js简单正则验证汉字英文及下划线的方法
2016/11/28 Javascript
详解基于Angular4+ server render(服务端渲染)开发教程
2017/08/28 Javascript
vue-cli脚手架build目录下utils.js工具配置文件详解
2018/09/14 Javascript
详解vue-router数据加载与缓存使用总结
2018/10/29 Javascript
记一次vue跨域的解决
2020/10/21 Javascript
JavaScript点击按钮生成4位随机验证码
2021/01/28 Javascript
[56:45]DOTA2上海特级锦标赛D组小组赛#1 EG VS COL第一局
2016/02/28 DOTA
[49:21]完美世界DOTA2联赛循环赛 Ink Ice vs LBZS BO2第二场 11.05
2020/11/06 DOTA
python获取当前计算机cpu数量的方法
2015/04/18 Python
Python实现按当前日期(年、月、日)创建多级目录的方法
2018/04/26 Python
python读取图片的方式,以及将图片以三维数组的形式输出方法
2019/07/03 Python
ML神器:sklearn的快速使用及入门
2019/07/11 Python
python Kmeans算法原理深入解析
2019/08/23 Python
Python3实现个位数字和十位数字对调, 其乘积不变
2020/05/03 Python
如何基于Python按行合并两个txt
2020/11/03 Python
html5 postMessage解决跨域、跨窗口消息传递方案
2016/12/20 HTML / CSS
全球最大的在线旅游公司:Expedia
2017/11/16 全球购物
汽车技术服务与营销专业在籍生自荐信
2013/09/28 职场文书
在求职信中如何凸显个人优势
2013/10/30 职场文书
新领导上任欢迎词
2014/01/13 职场文书
社区党员公开承诺书
2014/08/30 职场文书
2014年纪检工作总结
2014/11/12 职场文书
2014年预算员工作总结
2014/12/05 职场文书
委托开发合同书(标准版)
2019/08/07 职场文书
如何利用STAR法则制作留学文书?
2019/08/26 职场文书
pytorch中[..., 0]的用法说明
2021/05/20 Python
ant design charts 获取后端接口数据展示
2022/05/25 Javascript