深入理解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 相关文章推荐
判断页面是关闭还是刷新的js代码
Jan 28 Javascript
JavaScript 变量命名规则
Sep 23 Javascript
锋利的jQuery 第三章章节总结的例子
Mar 23 Javascript
Jjcarousellite 实现图片列表滚动的简单实例
Nov 29 Javascript
js通过更改按钮的显示样式实现按钮的滑动效果
Apr 23 Javascript
jQuery实现新消息在网页标题闪烁提示
Jun 23 Javascript
JavaScript基础篇(3)之Object、Function等引用类型
Nov 30 Javascript
详解JS中Array对象扩展与String对象扩展
Jan 07 Javascript
基于vue开发的在线付费课程应用过程
Jan 25 Javascript
Node.js命令行/批处理中如何更改Linux用户密码浅析
Jul 22 Javascript
基于JS实现数字动态变化显示效果附源码
Jul 18 Javascript
JavaScript交换变量常用4种方法解析
Sep 02 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
谈一谈收音机的高放电路
2021/03/02 无线电
Mysql的常用命令
2006/10/09 PHP
转换中文日期的PHP程序
2006/10/09 PHP
一个没有MYSQL数据库支持的简易留言本的编写
2006/10/09 PHP
ThinkPHP应用模式扩展详解
2014/07/16 PHP
php语言中使用json的技巧及json的实现代码详解
2015/10/27 PHP
PHP请求远程地址设置超时时间的解决方法
2016/10/29 PHP
jquery UI 1.72 之datepicker
2009/12/29 Javascript
javascript 基础篇2 数据类型,语句,函数
2012/03/14 Javascript
javascript清空table表格的方法
2015/05/14 Javascript
jQuery实现HTML表格单元格的合并功能
2016/04/06 Javascript
jQuery+CSS实现一个侧滑导航菜单代码
2016/05/09 Javascript
深入理解Angular2 模板语法
2016/08/07 Javascript
用jquery快速解决IE输入框不能输入的问题
2016/10/04 Javascript
Node.js如何响应Ajax的POST请求并且保存为JSON文件详解
2017/03/10 Javascript
微信小程序 商城开发(ecshop )简单实例
2017/04/07 Javascript
Vue.js 2.0 移动端拍照压缩图片预览及上传实例
2017/04/27 Javascript
基于jQuery的表单填充实例
2017/08/22 jQuery
vue 父组件中调用子组件函数的方法
2019/06/06 Javascript
es6函数中的作用域实例分析
2020/04/18 Javascript
JavaScript console的使用方法实例分析
2020/04/28 Javascript
小程序实现背景音乐播放和暂停
2020/06/19 Javascript
[03:55]TI9战队采访——TNC Predator
2019/08/22 DOTA
[38:32]完美世界DOTA2联赛循环赛 Forest vs DM 第二场 11.06
2020/11/06 DOTA
在python中画正态分布图像的实例
2019/07/08 Python
python查看文件大小和文件夹内容的方法
2019/07/08 Python
python连接PostgreSQL数据库的过程详解
2019/09/18 Python
python加载自定义词典实例
2019/12/06 Python
PyQt5实现登录页面
2020/05/30 Python
Eclipse面试题
2014/03/22 面试题
医院护士求职自荐信格式
2013/09/21 职场文书
经理秘书岗位职责
2013/11/14 职场文书
积极分子思想汇报
2014/01/04 职场文书
校运会入场式解说词
2014/02/10 职场文书
文明村创建实施方案
2014/03/27 职场文书
电气工程及其自动化专业求职信
2014/06/23 职场文书