深入理解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 相关文章推荐
Jquery 基础学习笔记之文档处理
May 29 Javascript
jquery中get和post的简单实例
Feb 04 Javascript
JavaScript中输出标签的方法
Aug 27 Javascript
让html页面不缓存js的实现方法
Oct 31 Javascript
利用JavaScript的AngularJS库制作电子名片的方法
Jun 18 Javascript
浅析Node.js中的内存泄漏问题
Jun 23 Javascript
JQuery EasyUI Layout 在from布局自适应窗口大小的实现方法
May 28 Javascript
JavaScript使用readAsDataURL读取图像文件
May 10 Javascript
vue中如何创建多个ueditor实例教程
Nov 14 Javascript
Openlayers实现点闪烁扩散效果
Sep 24 Javascript
微信小程序实现音乐播放页面布局
Dec 11 Javascript
HTML+JS实现在线朗读器
Feb 15 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三层结构(上) 简单三层结构
2010/07/04 PHP
destoon整合UCenter图文教程
2014/06/21 PHP
php获取CSS文件中图片地址并下载到本地的方法
2014/12/02 PHP
php实现文本数据导入SQL SERVER
2015/05/17 PHP
分享php多功能图片处理类
2016/05/15 PHP
ThinkPHP实现简单登陆功能
2017/04/28 PHP
php中的钩子理解及应用实例分析
2019/08/30 PHP
40个有创意的jQuery图片和内容滑动及弹出插件收藏集之二
2011/12/31 Javascript
JavaScript插入动态样式实现代码
2012/02/22 Javascript
jquery中filter方法用法实例分析
2015/02/06 Javascript
10个很棒的jQuery代码片段
2015/09/24 Javascript
jquery特效 点击展示与隐藏全文
2015/12/09 Javascript
Javascript实现Array和String互转换的方法
2015/12/21 Javascript
jQuery遍历DOM节点操作之filter()方法详解
2016/04/14 Javascript
Jquery揭秘系列:ajax原生js实现详解(推荐)
2016/06/08 Javascript
JS检测数组类型的方法小结
2017/03/14 Javascript
详解一个基于套接字实现长连接的express
2019/03/28 Javascript
基于vue-draggable 实现三级拖动排序效果
2020/01/10 Javascript
vuex存取值和映射函数使用说明
2020/07/24 Javascript
python的Template使用指南
2014/09/11 Python
python压缩文件夹内所有文件为zip文件的方法
2015/06/20 Python
Python无损音乐搜索引擎实现代码
2018/02/02 Python
Django中的文件的上传的几种方式
2018/07/23 Python
Python3分析处理声音数据的例子
2019/08/27 Python
在pycharm中显示python画的图方法
2019/08/31 Python
python中open函数的基本用法示例
2019/09/07 Python
python bluetooth蓝牙信息获取蓝牙设备类型的方法
2019/11/29 Python
在python shell中运行python文件的实现
2019/12/21 Python
python实现跨年表白神器--你值得拥有
2021/01/04 Python
微信小程序实现可实时改变转速的css3旋转动画实例代码
2018/09/11 HTML / CSS
美国诺德斯特龙百货官网:Nordstrom
2016/08/23 全球购物
澳大利亚Mocha官方网站:包、钱包、珠宝和配饰
2019/07/18 全球购物
幼儿园教师培训方案
2014/02/04 职场文书
军训自我鉴定100字
2014/02/13 职场文书
工作说明书范文
2014/05/07 职场文书
SQL Server表分区删除详情
2021/10/16 SQL Server