深入理解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',
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
方法的结论有
Component
方法实现了重载,接受不同的和Vue
相关类型Component
方法内部根据传入参数的类型不同,做不同的处理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的源码解析
看了上面Object
和Vue
语法后,我们再看component.ts
中的代码,我们得出componentFactory
方法的结论有
componentFactory
方法,在遍历形参,即Vue组件的Component
componentFactory
方法,根据变量Component
,生成组件的配置变量options
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
和其中的注意事项