欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

深入理解Vue中的Typescript(二)-vue_component源码解读

程序员文章站 2022-03-26 08:49:31
...

vue_component源码分析和Typescript语法

想查看更多的文章请关注公众号:IT巡游屋
深入理解Vue中的Typescript(二)-vue_component源码解读

1.概述

接着上篇文章,我们在Typescript定义一个组件,可以将组件定义成下面类样式

<template>
    <div>
    	<button @click="handleClick">{{count}}</button>
        <hello-world></hello-world>
    </div>
</template>
<script>
import Vue from 'vue'
import Component from 'vue-class-component'    
import HelloWorld = from './HelloWorld.vue'
    
@Component({
    components: {
        'hello-world': HelloWorld
    }    
})
export default class Counter extends Vue {
    count = 0   
    created(){
      	this.count = 1  
    }
    handleClick(){
        this.count++
    }
}
</script>

上代码用到@Component修饰符,而这个修饰符来源于vue-class-component项目,我们从它源代码的角度,看下它到底做了些什么?下面是这个项目的地址https://github.com/vuejs/vue-class-component

2.index.ts的源码预览

2.1入口文件index.js

首先看下这个项目的入口文件src/index.ts

import Vue, { ComponentOptions } from 'vue'
import { VueClass } from './declarations'
import { componentFactory, $internalHooks } from './component'

export { createDecorator, VueDecorator, mixins } from './util'

function Component <V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC
function Component <VC extends VueClass<Vue>>(target: VC): VC
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    //对类样式定义的组件做处理
    return componentFactory(Component, options)
  }
}

Component.registerHooks = function registerHooks (keys: string[]): void {
  $internalHooks.push(...keys)
}

export default Component

分析上面代码,不需要弄明白所有代码的细节,需要抓住要点,看懂两点:

(1) Component方法定义

(2) componentFactory方法作用

即要弄懂下面语句

// (1)Component方法的定义
function Component <V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC
function Component <VC extends VueClass<Vue>>(target: VC): VC
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    //(2)componentFactory方法的作用
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    //(2)componentFactory方法的作用
    return componentFactory(Component, options)
  }
}

要弄懂上面语句,我们得明白Typescript语法.下面对这两部分的语法进行讲解

3.Typescript语法

3.1 方法的重载

首先方法的重载的含义是指,可以定义同名的方法,在调用方法的时候,根据传参不同,调用不同的方法.但是在原生javasript当中不支持方法的重载,例如下面语句

function fn (a) { //第1个方法,接受1个参数
    console.log(a)
}
function fn (a, b) { //第2个方法,覆盖之第一个方法,接受2个参数
    console.log(a,b)
}

fn(1) //始终调用第2个方法

如果要根据参数不同执行不同的结果,将2个方法合并成一个方法,那么在原生javascript当中应该写成下面样式

function fn(a, b){
    if(b!==undefined){
        console.log(a, b)
    }else{
        console.log(a)
    }
}

typescript中,不能改变javascript不支持方法重载的情况,但在定义方法和使用方法的时候,做语法验证和更容易读懂,例如下typescript语句

function fn(a); //方法调用形式1,接收1个参数
function fn(a,b); //方法调用形式2,接收2个参数
function fn(a,b?){ //最终的函数定义,2种形式的结合.参数后面加'?',代表这个参数非必传参数
    if(b){
        console.log(a, b)
    }else{
        console.log(a)
    }
}
fn(1) //正确
fn(1,2) //正确
fn() //错误,编辑器报错,不符合函数定义
fn(1,2,3) //错误

3.2 变量类型的检查

typescript最大的语法特性,就是将javascript变量类型,在声明的时候进行限定,如果改变变量的类型,将会报错,这样做让javascript更加不容易出错,例如

let isDone: boolean //指定变量为布尔类型
isDone = true //正确
isDone = 'hello' //错误,不能改变数据的类型

下面整理常见的数据类型的定义,如下

3.2.1 普通数据类型
let isDone: boolean //布尔值
let num: number //数字
let username: string //字符串
let unusable: void //空值(undefined或者null)
let numArr: number[] //数组,存储数字
let a: any  //任意值,任意类型
let b: string | number // 联合类型,指定多个类型之一
3.2.2 函数数据类型
  • 方式1,有名字的函数,指定形参类型返回值数据类型
function sum(x: number, y: number): number { 
    return x + y
}
  • 方式2,函数变量,指定形参类型返回值数据类型
let sum: (x: number, y: number) => number 
sum = function (x, y) {
    return x + y
}
3.2.3 对象数据类型
  • 方式1-1,通过接口interface定义对象类型,检查自变量
interface Person { //检查对象,是否包含username,age属性,say方法
    username: string
    age: number
    say: (message: string) => string
}
let tom: Person 
tom = { //正确
    username: 'Tom',
    age: 25,
    say: function(message){
        return message
    }
}
  • 方式1-2,通过接口interface定义对象类型,检查类实例对象
interface PersonInterface { //检查类实例对象,是否包含username,age属性,say方法
    username: string
    age: number
    say: (message: string) => string
}
class Person{ //定义类型
    constructor(username: string, age: number){
        this.username = username
        this.age = age
    }
    username: string
    age: number
    say(message){
        return message
    }
}

let tom:PersonInterface
tom = new Person('zs',25) //正确
  • 方式2-1,通过关键字type定义对象类型,检查自变量
type Person = { //检查对象,是否包含username,age属性,say方法
    username: string
    age: number
    say: (message: string) => string
}
let tom: Person 
tom = { //正确
    username: 'Tom',
    age: 25,
    say: function(message){
        return message
    }
}
  • 方式2-2,通过关键字type定义对象类型,检查类实例对象
type PersonInterface = { //检查类实例对象,是否包含username,age属性,say方法
    username: string
    age: number
    say: (message: string) => string
}
class Person{ //定义类型
    constructor(username: string, age: number){
        this.username = username
        this.age = age
    }
    username: string
    age: number
    say(message){
        return message
    }
}

let tom:PersonInterface
tom = new Person('zs',25) //正确

3.3 泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

  • 方式1-在数组中使用
let arr: Array<number> //指定数组存储的数据类型
arr = [1,2,3] //正确
  • 方式2-在方法中使用
function createArray<T>(length: number, value: T): Array<T> { //指定形参和返回值的数据类型
    let result: T[] = []
    for (let i = 0; i < length; i++) {
        result[i] = value
    }
    return result
}
createArray<string>(3, 'x') //动态设置泛型'T'为string,返回数据为['x', 'x', 'x']
createArray<number>(2, 0) //动态设置泛型'T'为number,[0, 0]
  • 方式3-在类定义中使用
class GenericNumber<T> { //指定类中变量和方法使用的类型
    zeroValue: T;
    add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();//设置泛型'T'为number
myGenericNumber.zeroValue = 0; //正确
myGenericNumber.add = function(x, y) { return x + y; } //正确

4.index.ts的源码解析

看了上面typescript语法后,我们再看index.ts中的代码,我们得出Component方法的结论有

  1. Component方法实现了重载,接受不同的和Vue相关类型
  2. Component方法内部根据传入参数的类型不同,做不同的处理
  3. Component方法返回经过componentFactory处理后的数据
// (1)`Component`方法实现了重载,接受不同的和`Vue`相关类型
// 形参为跟Vue配置属性类.返回值为一个方法,接收Vue的子类 
function Component <V extends Vue>(options: ComponentOptions<V> & ThisType<V>): <VC extends VueClass<V>>(target: VC) => VC
// 形参为Vue的子类.返回值为Vue的子类
function Component <VC extends VueClass<Vue>>(target: VC): VC
// 形参为Vue的配置属性类或者Vue的子类,返回值为任意值
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  //(2)`Component`方法内部根据传入参数的类型不同,做不同的处理
  if (typeof options === 'function') {
    //(3)`Component`方法返回经过`componentFactory`处理后的数据
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    //(3)`Component`方法返回经过`componentFactory`处理后的数据
    return componentFactory(Component, options)
  }
}

5.component.ts的源码预览

接下来,我们看下src/component.ts的源码,看下componentFactory方法的定义,弄明白这个函数做了什么

export const $internalHooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render',
  'errorCaptured', // 2.5
  'serverPrefetch' // 2.6
]
export function componentFactory (
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  options.name = options.name || (Component as any)._componentTag || (Component as any).name
  // prototype props.
  const proto = Component.prototype
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    if (key === 'constructor') {
      return
    }

    // hooks
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    if (descriptor.value !== void 0) {
      // methods
      if (typeof descriptor.value === 'function') {
        (options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // typescript decorated data
        (options.mixins || (options.mixins = [])).push({
          data (this: Vue) {
            return { [key]: descriptor.value }
          }
        })
      }
    } else if (descriptor.get || descriptor.set) {
      // computed properties
      (options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set
      }
    }
  })

  // add data hook to collect class properties as Vue instance's data
  ;(options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return collectDataFromConstructor(this, Component)
    }
  })

  // decorate options
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }

  // find super
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
  const Extended = Super.extend(options)

  forwardStaticMembers(Extended, Component, Super)

  if (reflectionIsSupported()) {
    copyReflectionMetadata(Extended, Component)
  }

  return Extended
}

分析上面代码,我们也不需要弄明白所有代码的细节,需要抓住要点,看懂两点:

(1)componentFactory方法对传入的参数Component做了什么

(2)componentFactory方法返回什么样的数据

要弄懂上面语句,我们得明白上面component.ts当中一些es6的Object和vue当中的高级语法,下面对2者做讲解

6. ES6-Object语法

6.1 Object.getOwnPropertyDescriptor方法

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符.

其中自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性.

属性描述符是指对象属性的特征描述,包括4个特征

  • configurable:当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为true。

  • enumerable: 当且仅当指定对象的属性可以被枚举出时,为 true。

  • value: 该属性的值(仅针对数据属性描述符有效)

  • writable: 当且仅当属性的值可以被改变时为true

如下面示例

var user = {
    username: 'zs'
}
const descriptor = Object.getOwnPropertyDescriptor(user, 'username')
/*
输入为一个对象,对象为
{
    configurable: true
    enumerable: true
    value: "zs"
    writable: true
}
*/
console.log(descriptor)

6.2 Object.getOwnPropertyNames方法

Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名组成的数组

如下面示例

var user = {
    username: 'zs',
    age: 20
}
var names = Object.getOwnPropertyNames(user)
console.log(names) //['username','age']

6.3 Object.getPrototypeOf方法

Object.getPrototypeOf() 方法返回指定对象的原型

如下面示例

class Person {
    constructor(username, age){
        this.username = username
        this.age = age
    }
    say(){

    }
}
var p = new Person('zs', 20)
/*
输出
{
	constructor:f, 
	say: f
}
*/
console.log(Object.getPrototypeOf(p))

7.Vue-extend方法

Vue.extend()方法使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象

如下面示例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js"></script>
</head>
<body>
    <div id="app">
        
    </div>
</body>
<script>
var App = Vue.extend({
  template: '<p>{{firstName}} {{lastName}}</p>',
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White'
    }
  }
})
// 创建 App 实例,并挂载到一个元素上。
new App().$mount('#app')
</script>
</html>

8.component.ts的源码解析

看了上面ObjectVue语法后,我们再看component.ts中的代码,我们得出componentFactory方法的结论有

  1. componentFactory方法,在遍历形参,即Vue组件的Component
  2. componentFactory方法,根据变量Component,生成组件的配置变量options
  3. componentFactory方法,通过Vue.extend方法和配置变量options生成Vue的子类,并且返回该类
//构造函数的名称列表
export const $internalHooks = [
  //...省略部分次要代码
  'created',
  'mounted',
  //...省略部分次要代码
]
// 
export function componentFactory (
  Component: VueClass<Vue>, //形参Component为Vue组件类的对象
  options: ComponentOptions<Vue> = {} //形参optionsVue为组件配置属性对象,默认为空对象
): VueClass<Vue> { //返回值为Vue对象
    // ...省略部分次要代码
    // 给组件配置添加name属性
    options.name = options.name || (Component as any)._componentTag || (Component as any).name
  const proto = Component.prototype
  // 要点1.在遍历形参Component的属性
  Object.getOwnPropertyNames(proto).forEach(function (key) {
	// 要点2.生成组件的配置变量`options`
    // 给组件配置添加钩子函数属性
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }  
    // 得到属性描述  
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    if (descriptor.value !== void 0) {
        // 给组件配置添加methods属性
        if (typeof descriptor.value === 'function') {
        	(options.methods || (options.methods = {}))[key] = descriptor.value
      	}else if (descriptor.get || descriptor.set) {
            //给组件配置添加computed属性
            (options.computed || (options.computed = {}))[key] = {
                get: descriptor.get,
                set: descriptor.set
            }
        }
    }
    // ...省略部分次要代码
    // 得到父类即Vue类
    const superProto = Object.getPrototypeOf(Component.prototype)
    const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
    // 要点3.通过`Vue.extend`方法和配置变量`options`生成Vue的子类,并且返回该类
    // 调用父类的extend方法,即通过Vue.extend(options)生成Vue的子类
    const Extended = Super.extend(options)  
    // 返回处理生成的Vue对象
    return Extended
  })
}

9.自己写一个简单的vue-class-component

9.1 第一步,创建项目,安装依赖,写配置文件

  • 创建文件夹write-vue-class-component

  • 执行npm init -y生成package.json

  • 安装babel的依赖

    npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/node

    npm install --save @babel/polyfill

    npm install --save-dev @babel/plugin-proposal-decorators

    npm install --save-dev @babel/plugin-proposal-class-properties

  • 安装vue依赖

    npm install vue

  • 创建babel.config.js

const presets = [
    ["@babel/env",{
        targets:{
            edge:"17",
            firefox:"60",
            chrome:"67",
            safari:"11.1"
        }
    }]
]
const plugins = [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
]
module.exports = { presets, plugins }

9.2 创建装饰器component.js

import Vue from 'vue'
//构造函数的名称列表   
const $internalHooks = [ 
    'created',
    'mounted'
]
function componentFactory (Component, options = {}) { 
    const proto = Component.prototype
    // 遍历形参Component的属性
    Object.getOwnPropertyNames(proto).forEach(function (key) {
      // 给组件配置添加钩子函数属性
      if ($internalHooks.indexOf(key) > -1) {
        options[key] = proto[key]
        return
      }  
      // 得到属性描述 
      const descriptor = Object.getOwnPropertyDescriptor(proto, key)
      if (descriptor.value !== void 0) {
          // 给组件配置添加methods属性
          if (typeof descriptor.value === 'function') {
              (options.methods || (options.methods = {}))[key] = descriptor.value
          }else if (descriptor.get || descriptor.set) {
              //给组件配置添加computed属性
              (options.computed || (options.computed = {}))[key] = {
                  get: descriptor.get,
                  set: descriptor.set
           	  }
          }
        }      
        //通过Vue.extend(options)生成Vue的子类
        const Extended = Vue.extend(options)
        // 返回处理生成的Vue对象
        return Extended
    })
}

function Component (options) {
    if (typeof options === 'function') {
      return componentFactory(options)
    }
    return function (Component) {
      return componentFactory(Component, options)
    }
}
export default Component

9.3 创建测试代码index.js

import Vue from 'vue'
import Component from './component.js'    

@Component({
    filters: { //定义过滤器
        upperCase: function (value) {
            return value.toUpperCase()
        }
    }
})
class User extends Vue {
    firstName = ''//定义data变量
    lastName = ''  
    created(){ //定义生命周期函数
        this.firstName = 'li'
        this.lastName = 'lei'    
    }
    handleClick(){ //定义methods方法
        this.firstName = ''
        this.lastName = '' 
    }
    get fullName() { //定义计算属性
        return this.firstName + ' ' + this.lastName
    }
}

let u = new User()
console.log(u)

9.4 运行测试代码

npx babel-node index.js

运行成功,查看生成vue的对象

10.预告

弄清楚vue-class-component这个项目的核心代码和涉及到基础知识,和根据它的原理写了一个自己的vue-class-component后,下一节我们看下在项目当中如果使用vue-class-component和其中的注意事项

相关标签: 前端开发