vlambda博客
学习文章列表

深入理解Vue中的Typescript(二)

vue_component源码分析和Typescript语法

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>

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',
    age25,
    sayfunction(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',
    age25,
    sayfunction(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>(20//动态设置泛型'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, yreturn 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',
    age20
}
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>',
  datafunction ({
    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和其中的注意事项