一.TypeScript快速上手 (一). 初识 TypeScript TS与JS.png
初识TS.html#typescript-的介绍TypeScript 的介绍 TypeScript是一种由微软开发的开源、跨平台的编程语言。它是JavaScript的超集,最终会被编译为JavaScript代码。
2012年10月,微软发布了首个公开版本的TypeScript,2013年6月19日,在经历了一个预览版之后微软正式发布了正式版TypeScript
TypeScript的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言。
TypeScript扩展了JavaScript的语法,所以任何现有的JavaScript程序可以运行在TypeScript环境中。
TypeScript是为大型应用的开发而设计,并且可以编译为JavaScript。
TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持**,它由 Microsoft 开发,代码开源于 GitHub 上
TypeScript 是 JavaScript 的一个超集 ,主要提供了类型系统 和对 ES6+ 的支持 ,它由 Microsoft 开发,代码开源于 GitHub (opens new window) 上
TypeScript 的特点 TypeScript 主要有 3 大特点:
始于JavaScript,归于JavaScript
TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中。
类型系统 允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。
总结 TypeScript 在社区的流行度越来越高,它非常适用于一些大型项目,也非常适用于一些基础库,极大地帮助我们提升了开发效率和体验。
(二).TypeScript 常用语法 1. 基础类型 TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
布尔值 最基本的数据类型就是简单的 true/false 值,在JavaScript 和 TypeScript 里叫做 boolean
(其它语言中也一样)。
1 2 3 let isDone: boolean = false ; isDone = true ;
数字 和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015中引入的二进制和八进制字面量。
1 2 3 4 let a1: number = 10 let a2: number = 0b1010 let a3: number = 0o12 let a4: number = 0xa
字符串 JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string
表示文本数据类型。 和 JavaScript 一样,可以使用双引号("
)或单引号('
)表示字符串。
1 2 3 4 5 let name:string = 'tom' name = 'jack' let age:number = 12 const info = `My name is ${name} , I am ${age} years old!`
undefined 和 null TypeScript 里,undefined
和 null
两者各自有自己的类型分别叫做 undefined
和 null
。 它们的本身的类型用处不是很大:
1 2 let u: undefined = undefined let n: null = null
默认情况下 null
和 undefined
是所有类型的子类型。 就是说你可以把 null
和 undefined
赋值给 number
类型的变量。
数组 TypeScript 像 JavaScript 一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[]
,表示由此类型元素组成的一个数组:
1 let list1: number [] = [1 , 2 , 3 ]
第二种方式是使用数组泛型,Array<元素类型>
:
1 let list2: Array <number > = [1 , 2 , 3 ]
元组 Tuple 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同
。 比如,你可以定义一对值分别为 string
和 number
类型的元组。
1 2 3 let t1: [string , number ] t1 = ['hello' , 10 ] t1 = [10 , 'hello' ]
当访问一个已知索引的元素,会得到正确的类型:
1 2 console .log(t1[0 ].substring(1 )) console .log(t1[1 ].substring(1 ))
枚举 enum
类型是对 JavaScript 标准数据类型的一个补充。 使用枚举类型可以为一组数值赋予友好的名字
。
1 2 3 4 5 6 7 8 9 10 enum Color { Red, Green, Blue }let myColor: Color = Color.Green console .log(myColor, Color.Red, Color.Blue)
默认情况下,从 0
开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1
开始编号:
1 2 enum Color {Red = 1 , Green, Blue}let c: Color = Color.Green
或者,全部都采用手动赋值:
1 2 enum Color {Red = 1 , Green = 2 , Blue = 4 }let c: Color = Color.Green
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:
1 2 3 4 enum Color {Red = 1 , Green, Blue}let colorName: string = Color[2 ]console .log(colorName)
any 有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
1 2 3 let notSure: any = 4 notSure = 'maybe a string' notSure = false
在对现有代码进行改写的时候,any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。并且当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
1 2 3 let list: any [] = [1 , true , 'free' ] list[1 ] = 100
void 某种程度上来说,void
类型像是与 any
类型相反,它表示没有任何类型
。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
1 2 3 4 5 6 7 function fn ( ): void { console .log('fn()' ) }
声明一个 void
类型的变量没有什么大用,因为你只能为它赋予 undefined
和 null
:
1 let unusable: void = undefined
object object
表示非原始类型,也就是除 number
,string
,boolean
之外的类型。
使用 object
类型,就可以更好的表示像 Object.create
这样的 API
。例如:
1 2 3 4 5 6 7 8 9 function fn2 (obj:object ):object { console .log('fn2()' , obj) return {} }console .log(fn2(new String ('abc' )))console .log(fn2(String ))
联合类型 联合类型(Union Types)表示取值可以为多种类型中的一种 需求1: 定义一个一个函数得到一个数字或字符串值的字符串形式值
1 2 3 function toString2 (x: number | string ) : string { return x.toString() }
需求2: 定义一个一个函数得到一个数字或字符串值的长度
1 2 3 4 5 6 7 8 9 10 function getLength (x: number | string ) { if (x.length) { return x.length } else { return x.toString().length } }
类型断言 通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法, 另一个为 as
语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function getLength (x: number | string ) { if ((<string >x).length) { return (x as string ).length } else { return x.toString().length } }console .log(getLength('abcd' ), getLength(1234 ))
类型推断 类型推断: TS会在没有明确的指定类型的时候推测出一个类型 有下面2种情况: 1. 定义变量时赋值了, 推断为对应的类型. 2. 定义变量时没有赋值, 推断为any类型
1 2 3 4 5 6 7 8 let b9 = 123 let b10 b10 = 123 b10 = 'abc'
2.接口 TypeScript 的核心原则之一是对值所具有的结构进行类型检查。我们使用接口(Interfaces)来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)
接口初探 需求: 创建人的对象, 需要对人的属性进行一定的约束
1 2 3 4 id是number类型, 必须有, 只读的 name是string类型, 必须有 age是number类型, 必须有 sex是string类型, 可以没有
下面通过一个简单示例来观察接口是如何工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 interface IPerson { id : number name : string age : number sex : string }const person1: IPerson = { id : 1 , name : 'tom' , age : 20 , sex : '男' }
类型检查器会查看对象内部的属性是否与IPerson接口描述一致, 如果不一致就会提示类型错误。
可选属性 接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。
1 2 3 4 5 6 interface IPerson { id : number name : string age : number sex?: string }
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ?
符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。
1 2 3 4 5 6 const person2: IPerson = { id : 1 , name : 'tom' , age : 20 , }
只读属性 一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly
来指定只读属性:
1 2 3 4 5 6 interface IPerson { readonly id: number name : string age : number sex?: string }
一旦赋值后再也不能被改变了。
1 2 3 4 5 6 7 8 const person2: IPerson = { id : 2 , name : 'tom' , age : 20 , } person2.id = 2
readonly vs const 最简单判断该用 readonly
还是 const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const
,若做为属性则使用 readonly
。
函数类型 接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
1 2 3 4 5 6 7 interface SearchFunc { (source: string , subString : string ): boolean }
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
1 2 3 4 5 const mySearch: SearchFunc = function (source: string , sub: string ): boolean { return source.search(sub) > -1 }console .log(mySearch('abcd' , 'bc' ))
类类型 类实现接口 与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 interface Alarm { alert(): any ; }interface Light { lightOn(): void ; lightOff(): void ; }class Car implements Alarm { alert ( ) { console .log('Car alert' ); } }
一个类可以实现多个接口 1 2 3 4 5 6 7 8 9 10 11 class Car2 implements Alarm , Light { alert ( ) { console .log('Car alert' ); } lightOn ( ) { console .log('Car light on' ); } lightOff ( ) { console .log('Car light off' ); } }
接口继承接口 和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
1 2 3 interface LightableAlarm extends Alarm, Light { }
3. 类 对于传统的 JavaScript 程序我们会使用函数
和基于原型的继承
来创建可重用的组件,但对于熟悉使用面向对象方式的程序员使用这些语法就有些棘手,因为他们用的是基于类的继承
并且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ES6 开始, JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。
基本示例 下面看一个使用类的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Greeter { message : string constructor (message: string ) { this .message = message } greet (): string { return 'Hello ' + this .message } }const greeter = new Greeter('world' )console .log(greeter.greet())
如果你使用过 C# 或 Java,你会对这种语法非常熟悉。 我们声明一个 Greeter
类。这个类有 3 个成员:一个叫做 message
的属性,一个构造函数和一个 greet
方法。
你会注意到,我们在引用任何一个类成员的时候都用了 this
。 它表示我们访问的是类的成员。
后面一行,我们使用 new
构造了 Greeter
类的一个实例。它会调用之前定义的构造函数,创建一个 Greeter
类型的新对象,并执行构造函数初始化它。
最后一行通过 greeter
对象调用其 greet
方法
继承 在 TypeScript 里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Animal { run (distance: number ) { console .log(`Animal run ${distance} m` ) } }class Dog extends Animal { cry () { console .log('wang! wang!' ) } }const dog = new Dog() dog.cry() dog.run(100 )
这个例子展示了最基本的继承:类从基类中继承了属性和方法。 这里,Dog
是一个 派生类,它派生自 Animal
基类,通过 extends
关键字。 派生类通常被称作子类 ,基类通常被称作超类 。
因为 Dog
继承了 Animal
的功能,因此我们可以创建一个 Dog
的实例,它能够 cry()
和 run()
。
下面我们来看个更加复杂的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 class Animal { name : string constructor (name: string ) { this .name = name } run (distance: number =0 ) { console .log(`${this .name} run ${distance} m` ) } }class Snake extends Animal { constructor (name: string ) { super (name) } run (distance: number =5 ) { console .log('sliding...' ) super .run(distance) } }class Horse extends Animal { constructor (name: string ) { super (name) } run (distance: number =50 ) { console .log('dashing...' ) super .run(distance) } xxx () { console .log('xxx()' ) } }const snake = new Snake('sn' ) snake.run()const horse = new Horse('ho' ) horse.run()const tom: Animal = new Horse('ho22' ) tom.run()const tom3: Snake = new Animal('tom3' ) tom3.run()
这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends
关键字创建了 Animal的两个子类:Horse
和 Snake
。
与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super()
,它会执行基类的构造函数。 而且,在构造函数里访问 this
的属性之前,我们 一定要调用 super()
。 这个是 TypeScript 强制执行的一条重要规则。
这个例子演示了如何在子类里可以重写父类的方法。Snake
类和 Horse
类都创建了 run
方法,它们重写了从 Animal
继承来的 run
方法,使得 run
方法根据不同的类而具有不同的功能。注意,即使 tom
被声明为 Animal
类型,但因为它的值是 Horse
,调用 tom.run(34)
时,它会调用 Horse
里重写的方法。
1 2 3 4 sliding... sn run 5m dashing... ho run 50m
公共,私有与受保护的修饰符 默认为 public 在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public
来做修饰;例如,C# 要求必须明确地使用 public
指定成员是可见的。 在 TypeScript 里,成员都默认为 public
。
你也可以明确的将一个成员标记成 public
。 我们可以用下面的方式来重写上面的 Animal
类:
理解 private 当成员被标记成 private
时,它就不能在声明它的类的外部访问。
理解 protected protected
修饰符与 private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Animal { public name: string public constructor (name: string ) { this .name = name } public run (distance: number =0 ) { console .log(`${this .name} run ${distance} m` ) } }class Person extends Animal { private age: number = 18 protected sex: string = '男' run (distance: number =5 ) { console .log('Person jumping...' ) super .run(distance) } }class Student extends Person { run (distance: number =6 ) { console .log('Student jumping...' ) console .log(this .sex) super .run(distance) } }console .log(new Person('abc' ).name)
readonly 修饰符 你可以使用 readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
1 2 3 4 5 6 7 8 9 class Person { readonly name: string = 'abc' constructor (name: string ) { this .name = name } }let john = new Person('John' )
参数属性 在上面的例子中,我们必须在 Person
类里定义一个只读成员 name
和一个参数为 name
的构造函数,并且立刻将 name
的值赋给 this.name
,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person
类的修改版,使用了参数属性:
1 2 3 4 5 6 7 class Person2 { constructor (readonly name: string ) { } }const p = new Person2('jack' )console .log(p.name)
注意看我们是如何舍弃参数 name
,仅在构造函数里使用 readonly name: string
参数来创建和初始化 name
成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private
限定一个参数属性会声明并初始化一个私有成员;对于 public
和 protected
来说也是一样。
存取器 TypeScript
支持通过 getters/setters
来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用 get
和 set
。 首先,我们从一个没有使用存取器的例子开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Person { firstName : string = 'A' lastName : string = 'B' get fullName () { return this .firstName + '-' + this .lastName } set fullName (value) { const names = value.split('-' ) this .firstName = names[0 ] this .lastName = names[1 ] } }const p = new Person()console .log(p.fullName) p.firstName = 'C' p.lastName = 'D' console .log(p.fullName) p.fullName = 'E-F' console .log(p.firstName, p.lastName)
静态属性 到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static
定义 origin
,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin
前面加上类名。 如同在实例属性上使用 this.xxx
来访问属性一样,这里我们使用 Grid.xxx
来访问静态属性。
1 2 3 4 5 6 7 8 9 10 11 12 class Person { name1 : string = 'A' static name2: string = 'B' }console .log(Person.name2)console .log(new Person().name1)
抽象类 抽象类做为其它派生类的基类使用。 它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 abstract class Animal { abstract cry () run () { console .log('run()' ) } }class Dog extends Animal { cry () { console .log(' Dog cry()' ) } }const dog = new Dog() dog.cry() dog.run()
4. 函数 函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript 函数添加了额外的功能,让我们可以更容易地使用。
基本示例 和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:
1 2 3 4 5 6 7 8 9 function add (x, y ) { return x + y }let myAdd = function (x, y ) { return x + y; }
函数类型 为函数定义类型 让我们为上面那个函数添加类型:
1 2 3 4 5 6 7 function add (x: number , y: number ): number { return x + y }let myAdd = function (x: number , y: number ): number { return x + y }
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型。
书写完整函数类型 现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。
1 2 3 4 let myAdd2: (x: number , y: number ) => number = function (x: number , y: number ): number { return x + y }
可选参数和默认参数 TypeScript 里的每个函数参数都是必须的。 这不是指不能传递 null
或 undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined
。 在TypeScript 里我们可以在参数名旁使用 ?
实现可选参数的功能。 比如,我们想让 lastName
是可选的:
在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined
时。 它们叫做有默认初始化值的参数。 让我们修改上例,把firstName
的默认值设置为 "A"
。
1 2 3 4 5 6 7 8 9 10 11 function buildName (firstName: string ='A' , lastName?: string ): string { if (lastName) { return firstName + '-' + lastName } else { return firstName } }console .log(buildName('C' , 'D' ))console .log(buildName('C' ))console .log(buildName())
剩余参数 必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments
来访问所有传入的参数。
在 TypeScript 里,你可以把所有参数收集到一个变量里: 剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...
)后面给定的名字,你可以在函数体内使用这个数组。
1 2 3 4 function info (x: string , ...args: string [] ) { console .log(x, args) } info('abc' , 'c' , 'b' , 'a' )
函数重载 函数重载: 函数名相同, 而形参不同的多个函数 在JS中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的 但在TS中, 与其它面向对象的语言(如Java)就存在此语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function add (x: string , y: string ): string function add (x: number , y: number ): number // 定义函数实现 function add (x: string | number , y: string | number ): string | number { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 x + y if (typeof x === 'string' && typeof y === 'string' ) { return x + y } else if (typeof x === 'number' && typeof y === 'number' ) { return x + y } }console .log(add(1 , 2 ))console .log(add('a' , 'b' ))
5. 泛型 指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定具体类型的一种特性。
引入 下面创建一个函数, 实现功能: 根据指定的数量 count
和数据 value
, 创建一个包含 count
个 value
的数组 不用泛型的话,这个函数可能是下面这样:
1 2 3 4 5 6 7 8 9 10 11 function createArray (value: any , count: number ): any [] { const arr: any [] = [] for (let index = 0 ; index < count; index++) { arr.push(value) } return arr }const arr1 = createArray(11 , 3 )const arr2 = createArray('aa' , 3 )console .log(arr1[0 ].toFixed(), arr2[0 ].split('' ))
使用函数泛型 1 2 3 4 5 6 7 8 9 10 11 12 13 function createArray2 <T > (value: T, count: number ) { const arr: Array <T> = [] for (let index = 0 ; index < count; index++) { arr.push(value) } return arr }const arr3 = createArray2<number >(11 , 3 )console .log(arr3[0 ].toFixed())const arr4 = createArray2<string >('aa' , 3 )console .log(arr4[0 ].split('' ))
多个泛型参数的函数 一个函数可以定义多个泛型参数
1 2 3 4 5 function swap <K , V > (a: K, b: V ): [K , V ] { return [a, b] }const result = swap<string , number >('abc' , 123 )console .log(result[0 ].length, result[1 ].toFixed())
泛型接口 在定义接口时, 为接口中的属性或方法定义泛型类型 在使用接口时, 再指定具体的泛型类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 interface IbaseCRUD <T> { data : T[] add : (t: T ) => void getById : (id: number ) => T }class User { id?: number ; name: string ; age: number ; constructor (name, age ) { this .name = name this .age = age } }class UserCRUD implements IbaseCRUD <User > { data : User[] = [] add(user: User): void { user = {...user, id : Date .now()} this .data.push(user) console .log('保存user' , user.id) } getById(id: number ): User { return this .data.find(item => item.id===id) } }const userCRUD = new UserCRUD() userCRUD.add(new User('tom' , 12 )) userCRUD.add(new User('tom2' , 13 ))console .log(userCRUD.data)
泛型类 在定义类时, 为类中的属性或方法定义泛型类型 在创建类的实例时, 再指定特定的泛型类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class GenericNumber <T > { zeroValue : T add : (x: T, y: T ) => T }let myGenericNumber = new GenericNumber<number >() myGenericNumber.zeroValue = 0 myGenericNumber.add = function (x, y ) { return x + y }let myGenericString = new GenericNumber<string >() myGenericString.zeroValue = 'abc' myGenericString.add = function (x, y ) { return x + y }console .log(myGenericString.add(myGenericString.zeroValue, 'test' ))console .log(myGenericNumber.add(myGenericNumber.zeroValue, 12 ))
泛型约束 如果我们直接对一个泛型参数取 length
属性, 会报错, 因为这个泛型根本就不知道它有这个属性
1 2 3 4 function fn <T >(x: T ): void { }
我们可以使用泛型约束来实现
1 2 3 4 5 6 7 8 interface Lengthwise { length : number ; }function fn2 <T extends Lengthwise >(x: T ): void { console .log(x.length) }
我们需要传入符合约束类型的值,必须包含必须 length
属性:
6. 其它 声明文件 当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能
什么是声明语句
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script>
标签引入 jQuery
,然后就可以使用全局变量 $
或 jQuery
了。
但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西
1 2 3 4 5 6 7 8 9 10 jQuery('#foo' );
这时,我们需要使用 declare var 来定义它的类型
1 2 3 declare var jQuery: (selector: string ) => any ; jQuery('#foo' );
declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:
一般声明文件都会单独写成一个 xxx.d.ts
文件
创建 01_jQuery.d.ts
, 将声明语句定义其中, TS编译器会扫描并加载项目中所有的TS声明文件
1 declare var jQuery: (selector: string ) => any ;
很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx
, 可以在 https://www.npmjs.com/package/package
进行搜索
有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如jQuery/react)
内置对象 JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。
内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。
ECMAScript 的内置对象
Boolean Number String Date RegExp Error
1 2 3 4 5 6 7 8 9 let b: Boolean = new Boolean (1 )let n: Number = new Number (true )let s: String = new String ('abc' )let d: Date = new Date ()let r: RegExp = /^1/ let e: Error = new Error ('error message' ) b = true
BOM 和 DOM 的内置对象
Window Document HTMLElement DocumentFragment Event NodeList
1 2 3 4 5 6 const div: HTMLElement = document .getElementById('test' )const divs: NodeList = document .querySelectorAll('div' )document .addEventListener('click' , (event: MouseEvent ) => { console .dir(event.target) })const fragment: DocumentFragment = document .createDocumentFragment()
二、vue3快速上手 1. 认识Vue3 1) 了解相关信息
Vue.js 3.0 “One Piece” 正式版在今年9月份发布
2年多开发, 100+位贡献者, 2600+次提交, 600+次PR
Vue3支持vue2的大多数特性
更好的支持Typescript
2) 性能提升:
打包大小减少41%
初次渲染快55%, 更新渲染快133%
内存减少54%
使用Proxy代替defineProperty实现数据响应式
重写虚拟DOM的实现和Tree-Shaking
3) 新增特性
Composition (组合) API
setup
ref 和 reactive
computed 和 watch
新的生命周期函数
provide与inject
…
新组件
Fragment - 文档碎片
Teleport - 瞬移组件的位置
Suspense - 异步加载组件的loading界面
其它API更新
全局API的修改
将原来的全局API转移到应用对象
模板语法变化
2. 创建vue3项目 1) 使用 vue-cli 创建 文档: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create
1 2 3 4 5 6 ## 安装或者升级 npm install -g @vue/cli ## 保证 vue cli 版本在 4.5 .0 以上 vue --version ## 创建项目 vue create my-project
然后的步骤
Please pick a preset - 选择 Manually select features
Check the features needed for your project - 选择上 TypeScript ,特别注意点空格是选择,点回车是下一步
Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
Use class-style component syntax - 直接回车
Use Babel alongside TypeScript - 直接回车
Pick a linter / formatter config - 直接回车
Use history mode for router? - 直接回车
Pick a linter / formatter config - 直接回车
Pick additional lint features - 直接回车
Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
Save this as a preset for future projects? - 直接回车
2) 使用 vite 创建
1 2 3 4 npm init vite-app <project-name > cd <project-name > npm install npm run dev
三.Composition API 1. Composition API(常用部分) 文档:
https://composition-api.vuejs.org/zh/api.html
1) setup
新的option, 所有的组合API函数都在此使用, 只在初始化时执行一次
函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用
2) ref
作用: 定义一个数据的响应式
语法: const xxx = ref(initValue):
创建一个包含响应式数据的引用(reference)对象
js中操作数据: xxx.value
模板中操作数据: 不需要.value
一般用来定义一个基本类型的响应式数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <template > <h2 > {{count }} </h2 > <hr > <button @click ="update" > 更新</button > </template > <script > import { ref } from 'vue' export default { setup () { const count = ref(1 ) console .log(count) function update ( ) { count.value = count.value + 1 } return { count, update } } } </script >
3) reactive
作用: 定义多个数据的响应式
const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <template> <h2> name: {{state .name}}</h2> <h2> age: {{state .age}}</h2> <h2> wife: {{state .wife}}</h2> <hr> <button @click="update"> 更新</button> </template><script> /* reactive: 作用: 定义多个数据的响应式 const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象 响应式转换是“深层的”:会影响对象内部所有嵌套的属性 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的 */ import { reactive, } from 'vue' export default { setup () { /* 定义响应式数据对象 */ const state = reactive({ name: 'tom', age: 25 , wife: { name: 'marry', age: 22 }, }) console.log (state , state .wife) const update = () => { state .name += '--' state .age += 1 state .wife.name += '++' state .wife.age += 2 } return { state , update, } } } </script>
4) 比较Vue2与Vue3的响应式(重要) vue2的响应式
核心:
对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
1 2 3 4 Object .defineProperty (data, 'count' , { get () {}, set () {} })
问题
对象直接新添加的属性或删除已有属性, 界面不会自动更新
直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
Vue3的响应式
核心:
通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
文档:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 new Proxy(data, { // 拦截读取属性值 get (target, prop) { return Reflect.get(target, prop) }, // 拦截设置属性值或添加新属性 set (target, prop, value) { return Reflect.set(target, prop, value) }, // 拦截删除属性 deleteProperty (target, prop) { return Reflect.deleteProperty(target, prop) } }) proxy.name = 'tom' <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Proxy 与 Reflect</title > </head > <body > <script > const user = { name : "John" , age : 12 }; const proxyUser = new Proxy (user, { get (target, prop ) { console .log('劫持get()' , prop) return Reflect .get(target, prop) }, set (target, prop, val ) { console .log('劫持set()' , prop, val) return Reflect .set(target, prop, val); }, deleteProperty (target, prop) { console .log('劫持delete属性' , prop) return Reflect .deleteProperty(target, prop) } }); console .log(proxyUser===user) console .log(proxyUser.name, proxyUser.age) proxyUser.name = 'bob' proxyUser.age = 13 console .log(user) proxyUser.sex = '男' console .log(user) delete proxyUser.sex console .log(user) </script > </body > </html >
5) setup细节
setup执行的时机
在beforeCreate之前执行(一次), 此时组件对象还没有创建
this是undefined, 不能通过this来访问data/computed/methods / props
其实所有的composition API相关回调函数中也都不可以
setup的返回值
一般都返回一个对象: 为模板提供数据, 也就是模板中可以直接使用此对象中的所有属性/方法
返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性
返回对象中的方法会与methods中的方法合并成功组件对象的方法
如果有重名, setup优先
注意:
一般不要混合使用: methods中可以访问setup提供的属性和方法, 但在setup方法中不能访问data和methods
setup不能是一个async函数: 因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性数据
setup的参数
setup(props, context) / setup(props, {attrs, slots, emit})
props: 包含props配置声明且传入了的所有属性的对象
attrs: 包含没有在props配置中声明的属性的对象, 相当于 this.$attrs
slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
emit: 用来分发自定义事件的函数, 相当于 this.$emit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 <template > <h2 > App</h2 > <p > msg: {{msg }} </p > <button @click ="fn('--')" > 更新</button > <child :msg ="msg" msg2 ="cba" @fn ="fn" /> </template > <script lang ="ts" > import { reactive, ref, } from 'vue' import child from './child.vue' export default { components : { child }, setup () { const msg = ref('abc' ) function fn (content: string ) { msg.value += content } return { msg, fn } } } </script > <template > <div > <h3 > {{n }} </h3 > <h3 > {{m }} </h3 > <h3 > msg: {{msg }} </h3 > <h3 > msg2: {{$attrs.msg2 }} </h3 > <slot name ="xxx" > </slot > <button @click ="update" > 更新</button > </div > </template > <script lang ="ts" > import { ref, defineComponent } from 'vue' export default defineComponent({ name : 'child' , props : ['msg' ], emits : ['fn' ], data () { console .log('data' , this ) return { } }, beforeCreate () { console .log('beforeCreate' , this ) }, methods : { }, setup (props, {attrs, emit, slots}) { console .log('setup' , this ) console .log(props.msg, attrs.msg2, slots, emit) const m = ref(2 ) const n = ref(3 ) function update ( ) { m.value += 2 n.value += 2 emit('fn' , '++' ) } return { m, n, update, } }, }) </script >
6) reactive与ref-细节
是Vue3的 composition API中2个最重要的响应式API
ref用来处理基本类型数据, reactive用来处理对象(递归深度响应式)
如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象
ref内部: 通过给value属性添加getter/setter来实现对数据的劫持
reactive内部: 通过使用Proxy来实现对对象内部所有数据的劫持, 并通过Reflect操作对象内部数据
ref的数据操作: 在js中要.value, 在模板中不需要(内部解析模板时会自动添加.value)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <template > <h2 > App</h2 > <p > m1: {{m1 }} </p > <p > m2: {{m2 }} </p > <p > m3: {{m3 }} </p > <button @click ="update" > 更新</button > </template > <script lang ="ts" > import { reactive, ref } from 'vue' export default { setup () { const m1 = ref('abc' ) const m2 = reactive({x : 1 , y : {z : 'abc' }}) const m3 = ref({a1 : 2 , a2 : {a3 : 'abc' }}) console .log(m1, m2, m3) console .log(m3.value.a2) function update ( ) { m1.value += '--' m2.x += 1 m2.y.z += '++' m3.value = {a1 : 3 , a2 : {a3 : 'abc---' }} m3.value.a2.a3 += '==' console .log(m3.value.a2) } return { m1, m2, m3, update } } } </script >
7) 计算属性与监视
computed函数:
与computed配置功能一致
只有getter
有getter和setter
watch函数
与watch配置功能一致
监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
通过配置deep为true, 来指定深度监视
watchEffect函数
不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
默认初始时就会执行第一次, 从而可以收集需要监视的数据
监视数据发生变化时回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 <template > <h2 > App</h2 > fistName: <input v-model ="user.firstName" /> <br > lastName: <input v-model ="user.lastName" /> <br > fullName1: <input v-model ="fullName1" /> <br > fullName2: <input v-model ="fullName2" > <br > fullName3: <input v-model ="fullName3" > <br > </template > <script lang ="ts" > import { reactive, ref, computed, watch, watchEffect } from 'vue' export default { setup () { const user = reactive({ firstName : 'A' , lastName : 'B' }) const fullName1 = computed(() => { console .log('fullName1' ) return user.firstName + '-' + user.lastName }) const fullName2 = computed({ get () { console .log('fullName2 get' ) return user.firstName + '-' + user.lastName }, set (value: string) { console .log('fullName2 set' ) const names = value.split('-' ) user.firstName = names[0 ] user.lastName = names[1 ] } }) const fullName3 = ref('' ) watch(user, () => { fullName3.value = user.firstName + '-' + user.lastName }, { immediate : true , deep : true , }) watch(fullName3, (value ) => { console .log('watch' ) const names = value.split('-' ) user.firstName = names[0 ] user.lastName = names[1 ] }) watch([() => user.firstName, () => user.lastName, fullName3], (values ) => { console .log('监视多个数据' , values) }) return { user, fullName1, fullName2, fullName3 } } } </script >
8) 生命周期 vue2.x的生命周期
lifecycle_2
vue3的生命周期
lifecycle_3
与 2.x 版本生命周期相对应的组合式 API
beforeCreate
-> 使用 setup()
created
-> 使用 setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeDestroy
-> onBeforeUnmount
destroyed
-> onUnmounted
errorCaptured
-> onErrorCaptured
新增的钩子函数
组合式 API 还提供了以下调试钩子函数:
onRenderTracked
onRenderTriggered
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 <template > <div class ="about" > <h2 > msg: {{msg }} </h2 > <hr > <button @click ="update" > 更新</button > </div > </template > <script lang ="ts" > import { ref, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from "vue" export default { beforeCreate () { console .log('beforeCreate()' ) }, created () { console .log('created' ) }, beforeMount () { console .log('beforeMount' ) }, mounted () { console .log('mounted' ) }, beforeUpdate () { console .log('beforeUpdate' ) }, updated () { console .log('updated' ) }, beforeUnmount () { console .log('beforeUnmount' ) }, unmounted () { console .log('unmounted' ) }, setup ( ) { const msg = ref('abc' ) const update = () => { msg.value += '--' } onBeforeMount(() => { console .log('--onBeforeMount' ) }) onMounted(() => { console .log('--onMounted' ) }) onBeforeUpdate(() => { console .log('--onBeforeUpdate' ) }) onUpdated(() => { console .log('--onUpdated' ) }) onBeforeUnmount(() => { console .log('--onBeforeUnmount' ) }) onUnmounted(() => { console .log('--onUnmounted' ) }) return { msg, update } } } </script > <template > <h2 > App</h2 > <button @click ="isShow=!isShow" > 切换</button > <hr > <Child v-if ="isShow" /> </template > <script lang ="ts" > import Child from './Child.vue' export default { data () { return { isShow : true } }, components : { Child } } </script >
09) 自定义hook函数
使用Vue3的组合API封装的可复用的功能函数
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
需求1: 收集用户鼠标点击的页面坐标
hooks/useMousePosition.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import { ref, onMounted, onUnmounted } from 'vue' export default function useMousePosition ( ) { const x = ref(-1 ) const y = ref(-1 ) const updatePosition = (e: MouseEvent ) => { x.value = e.pageX y.value = e.pageY } onMounted(() => { document .addEventListener('click' , updatePosition) }) onUnmounted(() => { document .removeEventListener('click' , updatePosition) }) return {x, y} } <template><div > <h2 > x: {{x}}, y: {{y}}</h2 > </div > </template><script > import { ref } from "vue" import useMousePosition from './hooks/useMousePosition' export default { setup ( ) { const {x, y} = useMousePosition() return { x, y, } } } </script >
利用TS泛型强化类型检查
需求2: 封装发ajax请求的hook函数
hooks/useRequest.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 import { ref } from 'vue' import axios from 'axios' /* 使用axios发送异步ajax请求 */ export default function useUrlLoader<T > (url: string) { const result = ref<T | null > (null) const loading = ref(true) const errorMsg = ref(null) axios.get(url) .then(response => { loading.value = false result.value = response.data }) .catch(e => { loading.value = false errorMsg.value = e.message || '未知错误' }) return { loading, result, errorMsg, } } <template > <div class ="about" > <h2 v-if ="loading" > LOADING...</h2 > <h2 v-else-if ="errorMsg" > {{errorMsg }} </h2 > {{result.id }} {{result.name }} {{result.distance }} <ul v-for ="p in result" :key ="p.id" > <li > id: {{p.id }} </li > <li > title: {{p.title }} </li > <li > price: {{p.price }} </li > </ul > </div > </template > <script lang ="ts" > import { watch } from "vue" import useRequest from './hooks/useRequest' interface AddressResult { id : number; name: string; distance: string; } interface ProductResult { id : string; title: string; price: number; } export default { setup ( ) { const {loading, result, errorMsg} = useRequest<ProductResult[]>('/data/products.json' ) watch(result, () => { if (result.value) { console .log(result.value.length) } }) return { loading, result, errorMsg } } } </script >
10) toRefs 把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref
应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
问题: reactive 对象取出的所有属性值都是非响应式的
解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <template> <h2> App</h2> <h3> foo: {{foo}}</h3> <h3> bar: {{bar}}</h3> <h3> foo2: {{foo2}}</h3> <h3> bar2: {{bar2}}</h3> </template><script lang="ts"> import { reactive, to Refs } from 'vue' /*to Refs: 将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象 应用: 当从合成函数返回响应式对象时,to Refs 非常有用, 这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用 */ export default { setup () { const state = reactive({ foo: 'a', bar: 'b', }) const state AsRefs = to Refs(state ) set Timeout(() => { state .foo += '++' state .bar += '++' }, 2000 ); const {foo2, bar2} = useReatureX() return { // ...state , ...state AsRefs, foo2, bar2 } }, } function useReatureX() { const state = reactive({ foo2: 'a', bar2: 'b', }) set Timeout(() => { state .foo2 += '++' state .bar2 += '++' }, 2000 ); return to Refs(state ) } </script>
11) ref获取元素 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <template > <h2 > App</h2 > <input type ="text" > --- <input type ="text" ref ="inputRef" > </template > <script lang ="ts" > import { onMounted, ref } from 'vue' export default { setup ( ) { const inputRef = ref<HTMLElement|null >(null ) onMounted(() => { inputRef.value && inputRef.value.focus() }) return { inputRef } }, } </script >
2. Composition API(其它部分) 1) shallowReactive 与 shallowRef
shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
shallowRef: 只处理了value的响应式, 不进行对象的reactive处理
什么时候用浅响应式呢?
一般情况下使用ref和reactive即可
如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <template > <h2 > App</h2 > <h3 > m1: {{m1 }} </h3 > <h3 > m2: {{m2 }} </h3 > <h3 > m3: {{m3 }} </h3 > <h3 > m4: {{m4 }} </h3 > <button @click ="update" > 更新</button > </template > <script lang ="ts" > import { reactive, ref, shallowReactive, shallowRef } from 'vue' export default { setup () { const m1 = reactive({a : 1 , b : {c : 2 }}) const m2 = shallowReactive({a : 1 , b : {c : 2 }}) const m3 = ref({a : 1 , b : {c : 2 }}) const m4 = shallowRef({a : 1 , b : {c : 2 }}) const update = () => { m4.value.a += 1 } return { m1, m2, m3, m4, update, } } } </script >
2) readonly 与 shallowReadonly
readonly:
深度只读数据
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
只读代理是深层的:访问的任何嵌套 property 也是只读的。
shallowReadonly
浅只读数据
创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
应用场景:
在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <template> <h2> App</h2> <h3> {{state }}</h3> <button @click="update"> 更新</button> </template><script lang="ts"> import { reactive, readonly, shallowReadonly } from 'vue' /* readonly: 深度只读数据 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。 只读代理是深层的:访问的任何嵌套 property 也是只读的。 shallowReadonly: 浅只读数据 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 应用场景: 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除 */ export default { setup () { const state = reactive({ a: 1 , b: { c: 2 } }) // const rState1 = readonly(state ) const rState2 = shallowReadonly(state ) const update = () => { // rState1.a++ // error // rState1.b.c++ // error // rState2.a++ // error rState2.b.c++ } return { state , update } } } </script>
3) toRaw 与 markRaw
toRaw
返回由 reactive
或 readonly
方法转换成响应式代理的普通对象。
这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
markRaw
标记一个对象,使其永远不会转换为代理。返回对象本身
应用场景:
有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <template> <h2> {{state }}</h2> <button @click="testToRaw"> 测试to Raw</button> <button @click="testMarkRaw"> 测试markRaw</button> </template><script lang="ts"> /* to Raw: 得到reactive代理对象的目标数据对象 */ import { markRaw, reactive, to Raw, } from 'vue' export default { setup () { const state = reactive<any> ({ name: 'tom', age: 25 , }) const testToRaw = () => { const user = to Raw(state ) user .age++ // 界面不会更新 } const testMarkRaw = () => { const likes = ['a', 'b'] // state .likes = likes state .likes = markRaw(likes) // likes数组就不再是响应式的了 set Timeout(() => { state .likes[0 ] += '--' }, 1000 ) } return { state , testToRaw, testMarkRaw, } } } </script>
4) toRef
为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
应用: 当要将 某个prop 的 ref 传递给复合函数时,toRef 很有用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 <template > <h2 > App</h2 > <p > {{state }} </p > <p > {{foo }} </p > <p > {{foo2 }} </p > <button @click ="update" > 更新</button > <Child :foo ="foo" /> </template > <script lang ="ts" > import { reactive, toRef, ref, } from 'vue' import Child from './Child.vue' export default { setup () { const state = reactive({ foo : 1 , bar : 2 }) const foo = toRef(state, 'foo' ) const foo2 = ref(state.foo) const update = () => { state.foo++ } return { state, foo, foo2, update, } }, components : { Child } } </script > <template > <h2 > Child</h2 > <h3 > {{foo }} </h3 > <h3 > {{length }} </h3 > </template > <script lang ="ts" > import { computed, defineComponent, Ref, toRef } from 'vue' const component = defineComponent({ props : { foo : { type : Number , require : true } }, setup (props, context) { const length = useFeatureX(toRef(props, 'foo' )) return { length } } }) function useFeatureX (foo: Ref ) { const lenth = computed(() => foo.value.length) return lenth } export default component</script >
5) customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
需求: 使用 customRef 实现 debounce 的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <template > <h2 > App</h2 > <input v-model ="keyword" placeholder ="搜索关键字" /> <p > {{keyword }} </p > </template > <script lang ="ts" > import { ref, customRef } from 'vue' export default { setup () { const keyword = useDebouncedRef('' , 500 ) console .log(keyword) return { keyword } }, } function useDebouncedRef <T >(value: T, delay = 200 ) { let timeout: number return customRef((track, trigger ) => { return { get ( ) { track() return value }, set (newValue: T ) { clearTimeout (timeout) timeout = setTimeout (() => { value = newValue trigger() }, delay) } } }) } </script >
6) provide 与 inject
provide和
inject提供依赖注入,功能类似 2.x 的
provide/inject
实现跨层级组件(祖孙)间通信
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <template > <h1 > 父组件</h1 > <p > 当前颜色: {{color }} </p > <button @click ="color='red'" > 红</button > <button @click ="color='yellow'" > 黄</button > <button @click ="color='blue'" > 蓝</button > <hr > <Son /> </template > <script lang ="ts" > import { provide, ref } from 'vue' import Son from './Son.vue' export default { name : 'ProvideInject' , components : { Son }, setup ( ) { const color = ref('red' ) provide('color' , color) return { color } } } </script > <template > <div > <h2 > 子组件</h2 > <hr > <GrandSon /> </div > </template > <script lang ="ts" > import GrandSon from './GrandSon.vue' export default { components : { GrandSon }, } </script > <template > <h3 :style ="{color}" > 孙子组件: {{color }} </h3 > </template > <script lang ="ts" > import { inject } from 'vue' export default { setup ( ) { const color = inject('color' ) return { color } } } </script >
7) 响应式数据的判断
isRef: 检查一个值是否为一个 ref 对象
isReactive: 检查一个对象是否是由 reactive
创建的响应式代理
isReadonly: 检查一个对象是否是由 readonly
创建的只读代理
isProxy: 检查一个对象是否是由 reactive
或者 readonly
方法创建的代理
3. 手写组合API 1) shallowReactive 与 reactive 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 const reactiveHandler = { get (target, key ) { if (key ==='_is_reactive' ) return true return Reflect.get (target, key ) }, set (target, key , value) { const result = Reflect.set (target, key , value) console.log ('数据已更新, 去更新界面' ) return result }, deleteProperty (target, key ) { const result = Reflect.deleteProperty(target, key ) console.log ('数据已删除, 去更新界面' ) return result }, } function shallowReactive(obj) { return new Proxy(obj, reactiveHandler) } function reactive (target) { if (target && typeof target==='object' ) { if (target instanceof Array ) { target.forEach((item, index) => { target[index] = reactive(item) }) } else { Object .keys(target).forEach(key => { target[key ] = reactive(target[key ]) }) } const proxy = new Proxy(target, reactiveHandler) return proxy } return target }const proxy = shallowReactive({ a: { b: 3 } }) proxy.a = {b: 4 } proxy.a.b = 5 const obj = { a: 'abc' , b: [{x: 1 }], c: {x: [11 ]}, }const proxy = reactive(obj) console.log (proxy) proxy.b[0 ].x += 1 proxy.c.x[0 ] += 1
2) shallowRef 与 ref 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 function shallowRef(target) { const result = { _value: target, _is_ref: true , get value () { return this ._value }, set value (val ) { this ._value = val console.log('set value 数据已更新, 去更新界面' ) } } return result } function ref(target) { if (target && typeof target==='object' ) { target = reactive(target) } const result = { _value: target, _is_ref: true , get value () { return this ._value }, set value (val ) { this ._value = val console.log('set value 数据已更新, 去更新界面' ) } } return result }const ref3 = shallowRef({ a: 'abc' , }) ref3.value = 'xxx' ref3.value.a = 'yyy' const ref1 = ref(0 )const ref2 = ref({ a: 'abc' , b: [{x: 1 }], c: {x: [11 ]}, }) ref1.value++ ref2.value.b[0 ].x++ console.log(ref1, ref2)
3) shallowReadonly 与 readonly 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 const readonlyHandler = { get (target, key) { if (key==='_is_readonly' ) return true return Reflect .get(target, key) }, set () { console .warn('只读的, 不能修改' ) return true }, deleteProperty () { console .warn('只读的, 不能删除' ) return true }, }function shallowReadonly (obj ) { return new Proxy (obj, readonlyHandler) }function readonly (target ) { if (target && typeof target==='object' ) { if (target instanceof Array ) { target.forEach((item, index ) => { target[index] = readonly (item) }) } else { Object .keys(target).forEach(key => { target[key] = readonly (target[key]) }) } const proxy = new Proxy (target, readonlyHandler) return proxy } return target }const objReadOnly = readonly ({ a : { b : 1 } })const objReadOnly2 = shallowReadonly({ a : { b : 1 } }) objReadOnly.a = 1 objReadOnly.a.b = 2 objReadOnly2.a = 1 objReadOnly2.a.b = 2
4) isRef, isReactive 与 isReadonly 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function isRef(obj ) { return obj && obj._is_ref }function isReactive(obj ) { return obj && obj._is_reactive }function isReadonly(obj ) { return obj && obj._is_readonly }function isProxy (obj) { return isReactive(obj ) || isReadonly(obj ) } console.log(isReactive(reactive ({}) )) console.log(isRef(ref ({}) )) console.log(isReadonly(readonly ({}) )) console.log(isProxy(reactive ({}) )) console.log(isProxy(readonly ({}) ))
4. Composition API VS Option API 1) Option API的问题
在传统的Vue OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 ,滚动条反复上下移动
img
img
2) 使用Compisition API 我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起
img
四.其它新组合和API 1. 新组件 1) Fragment(片断)
在Vue2中: 组件必须有一个根标签
在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
好处: 减少标签层级, 减小内存占用
1 2 3 4 <template> <h2> aaaa </h2 > <h2> aaaa </h2 ></template>
2) Teleport(瞬移)
Teleport 提供了一种干净的方法, 让组件的html在父组件界面外的特定标签(很可能是body)下插入显示
ModalButton.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 <template > <button @click ="modalOpen = true" > Open full screen modal! (With teleport!) </button > <teleport to ="body" > <div v-if ="modalOpen" class ="modal" > <div > I'm a teleported modal! (My parent is "body") <button @click ="modalOpen = false" > Close </button > </div > </div > </teleport > </template > <script > import { ref } from 'vue' export default { name : 'modal-button' , setup () { const modalOpen = ref(false ) return { modalOpen } } } </script > <style > .modal { position : absolute; top : 0 ; right : 0 ; bottom : 0 ; left : 0 ; background-color : rgba (0 ,0 ,0 ,.5 ); display : flex; flex-direction : column; align-items : center; justify-content : center; } .modal div { display : flex; flex-direction : column; align-items : center; justify-content : center; background-color : white; width : 300px ; height : 300px ; padding : 5px ; } </style >
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template > <h2 > App</h2 > <modal-button > </modal-button > </template > <script lang ="ts" > import ModalButton from './ModalButton.vue' export default { setup ( ) { return { } }, components : { ModalButton } } </script >
3) Suspense(不确定的)
它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <template > <Suspense > <template v-slot:default > <AsyncComp /> </template > <template v-slot:fallback > <h1 > LOADING...</h1 > </template > </Suspense > </template > <script lang ="ts" > import AsyncAddress from './AsyncAddress.vue' import { defineAsyncComponent } from 'vue' const AsyncComp = defineAsyncComponent(() => import ('./AsyncComp.vue' ))export default { setup ( ) { return { } }, components : { AsyncComp, AsyncAddress } } </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template > <h2 > AsyncComp22</h2 > <p > {{msg }} </p > </template > <script lang ="ts" > export default { name : 'AsyncComp' , setup () { return { msg : 'abc' } } } </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template > <h2 > {{data }} </h2 > </template > <script lang ="ts" > import axios from 'axios' export default { async setup ( ) { const result = await axios.get('/data/address.json' ) return { data : result.data } } } </script >
2. 其他新的API 全新的全局API
createApp()
defineProperty()
defineAsyncComponent()
nextTick()
将原来的全局API转移到应用对象
app.component()
app.config()
app.directive()
app.mount()
app.unmount()
app.use()
模板语法变化
v-model的本质变化
prop:value -> modelValue;
event:input -> update:modelValue;
.sync修改符已移除, 由v-model代替
v-if优先v-for解析
五.使用VuePress搭建在线文档网站 0. 在线文档 VuePress官方在线文档
1. 搭建基本环境 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 将 VuePress 作为一个本地依赖安装 npm install -D vuepress# 新建一个 docs 文件夹 mkdir docs# 新建一个文件: docs/README.md echo '# Hello VuePress!' > docs/README.md# 启动文档项目 npx vuepress dev docs# 构建静态文件 npx vuepress build docs |-- docs |-- .vuepress |-- config.js |-- README.md
2. 配置ts教程文档
整体结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |-- dist |-- dics |-- .vuepress |-- public |-- ts-logo.png |-- config.js |-- chapter1 |-- 01_初识TS.md |-- 02_安装TS.md |-- 03_HelloWorld.md |-- chapter2 |-- 1_type.md |-- 2_interface.md |-- 3_class.md |-- 4_function.md |-- 5_generic.md |-- 6_other.md |-- chapter3 |-- 01_react.md |-- 02_vue.md |-- chapter4 |-- README.md |-- README.md |-- package.json
docs/.vuepress/config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 注意: base的值为github仓库的名称 module.exports = { base: '/ts-study/' , /* 基础虚拟路径: */ dest: 'dist' , /* 打包文件基础路径, 在命令所在目录下 */ title: 'TypeScript 入门' , // 标题 description: '学习使用 TypeScript' , // 标题下的描述 themeConfig: { // 主题配置 sidebar: [ // 左侧导航 { title: '初识 TypeScript' , // 标题 collapsable: false, // 下级列表不可折叠 children: [ // 下级列表 'chapter1/01_初识TS' , 'chapter1/02_安装TS' , 'chapter1/03_HelloWorld' ] }, { title: 'TypeScript 常用语法' , collapsable: false, children: [ 'chapter2/1_type' , 'chapter2/2_interface' , 'chapter2/3_class' , 'chapter2/4_function' , 'chapter2/5_generic' , ] }, ] } }
docs/README.md
1 2 3 4 5 6 7 8 9 10 --- home: true heroImage: /ts-logo.png actionText: 开始学习 → actionLink: /chapter1/01_初识TS ---
package.json
1 2 3 4 5 "scripts" : { "doc:dev" : "vuepress dev docs" , "doc:build" : "vuepress build docs" , "doc:deploy" : "gh-pages -d docs/dist" }
3. 发布到gitpage
使用git管理当前项目
将打包的项目推送到gitpage
1 2 3 4 5 6 # 下载工具包 yarn add -D gh-pages# 执行打包命令 yarn doc:build# 执行部署命令 yarn doc:deploy