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

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中主要用了类装饰器,本文只对类装饰器做简单介绍,更多信息请参阅阮老师的文章: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的实现原理也有了一个大体的思路。因本人技术有限,文中可能存在肤浅、错误的地方,如有发现,还请不吝赐教,感谢!

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

(0)

相关推荐

  • 详解TypeScript+Vue 插件 vue-class-component的使用总结

    首先 下载 npm install vue-class-component vue-property-decorator --save-dev 一梭子直接干: 其次,咱来说说它们的区别与联系: vue-property-decorator社区出品:vue-class-component官方出品 vue-class-component提供了Vue.Component等: vue-property-decorator深度依赖了vue-class-component,拓展出了更多操作符:@Prop.@

  • Vue 2源码阅读 Provide Inject 依赖注入详解

    目录 Provide/Inject 初始化 1. initInjections 依赖初始化 2. initProvide 注入数据初始化 总结 Provide/Inject 初始化 1. initInjections 依赖初始化 该步骤其实发生在 initState 之前,但是由于 provide/inject 一般是配合使用,所以这里调整了一下顺序. 该函数的定义与过程都比较简单: export function initInjections(vm: Component) { const re

  • vue-next/runtime-core 源码阅读指南详解

    写在前面 最近又抽时间把 vue-next/runtime-core 的源码陆陆续续地看完了,期间整理了很多笔记,但都是碎片化的.本来是想整理一下,写成一篇文章分享出来的,但是感觉最终的成果物只能是一篇篇幅巨长的解析文,就算我一行一行的把源码加上注释,其阅读体验也会很差,因为每个人读代码的习惯不同,思路不同.正所谓抛砖引玉,所以,我觉的写一篇向导文作为这块砖应该是足够了,希望可以帮助到想看源码但觉得无从看起.无从下手的读者. 另一方面,也算是给自己挖一个坑,因为这篇文章中涉及到的很多内容,三言两

  • 源码阅读之storm操作zookeeper-cluster.clj

    storm操作zookeeper的主要函数都定义在命名空间backtype.storm.cluster中(即cluster.clj文件中). backtype.storm.cluster定义了两个重要protocol:ClusterState和StormClusterState. clojure中的protocol可以看成java中的接口,封装了一组方法.ClusterState协议中封装了一组与zookeeper进行交互的基础函数,如获取子节点函数,获取子节点数据函数等,ClusterStat

  • Vue编译器optimize源码分析

    目录 引言 optimize 源码之旅 markStatic$1源码 isStatic源码 复杂点的 回归到markStatic$1 markStaticRoots 源码 引言 接上文 parseHTML 函数源码解析 chars.end.comment钩子函数 上一章节我们讲到通过解析将template转成AST(抽象语法树),接下来继续对模型树优化,进行静态标注.那么问题来了,什么是静态标注?为什么要静态标注. 在源码的注释中我们找到了下面这段话: /** * Goal of the opt

  • Three.js源码阅读笔记(Object3D类)

    这是Three.js源码阅读笔记的第二篇,直接开始. Core::Object3D Object3D似乎是Three.js框架中最重要的类,相当一部分其他的类都是继承自Object3D类,比如场景类.几何形体类.相机类.光照类等等:他们都是3D空间中的对象,所以称为Object3D类.Object3D构造函数如下: 复制代码 代码如下: THREE.Object3D = function () { THREE.Object3DLibrary.push( this ); this.id = THR

  • Three.js源码阅读笔记(物体是如何组织的)

    这是Three.js源码阅读笔记第三篇.之前两篇主要是关于核心对象的,这些核心对象主要围绕着矢量vector3对象和矩阵matrix4对象展开的,关注的是空间中的单个顶点的位置和变化.这一篇将主要讨论Three.js中的物体是如何组织的:即如何将顶点.表面.材质组合成为一个具体的对象. Object::Mesh 该构造函数构造了一个空间中的物体.之所以叫"网格"是因为,实际上具有体积的物体基本都是建模成为"网格"的. 复制代码 代码如下: THREE.Mesh =

  • java源码阅读之java.lang.Object

    Object是所有类的父类,任何类都默认继承Object.Object类到底实现了哪些方法? 1.clone方法 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常. 2.getClass方法 final方法,获得运行时类型. 3.toString方法 该方法用得比较多,一般子类都有覆盖. 4.finalize方法 该方法用于释放资源.因为无法确定该方法什么时候被调用,很少使用. 5.equals方法 该

  • springboot-2.3.x最新版源码阅读环境搭建(基于gradle构建)

    一.前言 跟很多小伙伴聊天,发现一个严重的问题,很多小伙伴横向发展的貌似很不错,很多技术都能说出一二,但是如果在某个技术上深挖一下就不行了,问啥啥不会.就拿springboot来说,很多同学止步于springboot的应用,再往深处就一问三不知了,那么如何破局呢?smart哥认为最好的办法就是直捣黄龙,要把一个技术理解透了,听别人讲一万遍原理,不如自己撕一遍源码. 要阅读源码那就首先得先搭建源码阅读环境,那么本篇文章就来介绍下Spring Boot的源码环境搭建. 鉴于spring团队已经全面抛

  • 一文帮你理解PReact10.5.13源码

    目录 render.js部分 create-context.js部分 diff部分 React源码看过几次,每次都没有坚持下来,索性学习一下PReact部分,网上讲解源码的不少,但是基本已经过时,所以自己来梳理下 render.js部分 import { EMPTY_OBJ, EMPTY_ARR } from './constants'; import { commitRoot, diff } from './diff/index'; import { createElement, Fragme

  • python json-rpc 规范源码阅读

    目录 json-rpc 源码阅读 JSON-RPC规范 jsonrpcclient的实现 jsonrpcserver的实现 小结 小技巧 json-rpc 源码阅读 JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议.JSON-RPC应用很广泛,比如以太坊的API.JSON-RPC的python实现较多,我选择了Exploding Labs 提供的python版本.主要是其它库都比较古老,而e-labs的实现采用最新版本python,支持类型系统,还有一些函数式编程的范式,代码也很

随机推荐