Vue3+TypeScript?看这一篇就够了

Vue3系列 专栏收录该内容
1 篇文章 2 订阅

1、TypeScript快速上手

1.1 初识 TypeScript

TS与JS.png

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

TypeScript 的特点

TypeScript 主要有 3 大特点:

  • 始于JavaScript,归于JavaScript

TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的JavaScript 引擎中。

  • 强大的类型系统

类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。

  • 先进的 JavaScript

TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

总结

TypeScript 在社区的流行度越来越高,它非常适用于一些大型项目,也非常适用于一些基础库,极大地帮助我们提升了开发效率和体验。

1.2 安装 TypeScript

命令行运行如下命令,全局安装 TypeScript:

npm install -g typescript

安装完成后,在控制台运行如下命令,检查安装是否成功(3.x):

tsc -V 

1.3. 第一个 TypeScript 程序

编写 TS 程序

src/helloworld.ts

function greeter (person) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

手动编译代码

我们使用了 .ts 扩展名,但是这段代码仅仅是 JavaScript 而已。

在命令行上,运行 TypeScript 编译器:

tsc helloworld.ts

输出结果为一个 helloworld.js 文件,它包含了和输入文件中相同的 JavsScript 代码。

在命令行上,通过 Node.js 运行这段代码:

node helloworld.js

控制台输出:

Hello, Yee

VsCode自动编译

1). 生成配置文件tsconfig.json
    tsc --init
2). 修改tsconfig.json配置
    "outDir": "./js",
    "strict": false,    
3). 启动监视任务: 
    终端 -> 运行任务 -> 监视tsconfig.json

类型注解

接下来让我们看看 TypeScript 工具带来的高级功能。 给 person 函数的参数添加 : string 类型注解,如下:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望 greeter 函数接收一个字符串参数。 然后尝试把 greeter 的调用改成传入一个数组:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = [0, 1, 2]

console.log(greeter(user))

重新编译,你会看到产生了一个错误:

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

类似地,尝试删除 greeter 调用的所有参数。 TypeScript 会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。

要注意的是尽管有错误,greeter.js 文件还是被创建了。 就算你的代码里有错误,你仍然可以使用 TypeScript。但在这种情况下,TypeScript 会警告你代码可能不会按预期执行。

接口

让我们继续扩展这个示例应用。这里我们使用接口来描述一个拥有 firstNamelastName 字段的对象。 在 TypeScript 里,只在两个类型内部的结构兼容,那么这两个类型就是兼容的。 这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements 语句。

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = {
  firstName: 'Yee',
  lastName: 'Huang'
}

console.log(greeter(user))

最后,让我们使用类来改写这个例子。 TypeScript 支持 JavaScript 的新特性,比如支持基于类的面向对象编程。

让我们创建一个 User 类,它带有一个构造函数和一些公共字段。因为类的字段包含了接口所需要的字段,所以他们能很好的兼容。

还要注意的是,我在类的声明上会注明所有的成员变量,这样比较一目了然。

class User {
  fullName: string
  firstName: string
  lastName: string

  constructor (firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
    this.fullName = firstName + ' ' + lastName
  }
}

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = new User('Yee', 'Huang')

console.log(greeter(user))

重新运行 tsc greeter.ts,你会看到 TypeScript 里的类只是一个语法糖,本质上还是 JavaScript 函数的实现。

总结

到这里,你已经对 TypeScript 有了一个大致的印象,那么下一章让我们来一起学习 TypeScript 的一些常用语法吧。

1.4 使用webpack打包TS

下载依赖

yarn add -D typescript
yarn add -D webpack webpack-cli
yarn add -D webpack-dev-server
yarn add -D html-webpack-plugin clean-webpack-plugin
yarn add -D ts-loader
yarn add -D cross-env

入口JS: src/main.ts

// import './01_helloworld'

document.write('Hello Webpack TS!')

index页面: public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>webpack & TS</title>
</head>
<body>
  
</body>
</html>

build/webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

const isProd = process.env.NODE_ENV === 'production' // 是否生产环境

function resolve (dir) {
  return path.resolve(__dirname, '..', dir)
}

module.exports = {
  mode: isProd ? 'production' : 'development',
  entry: {
    app: './src/main.ts'
  },

  output: {
    path: resolve('dist'),
    filename: '[name].[contenthash:8].js'
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        include: [resolve('src')]
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin({
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],

  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },

  devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',

  devServer: {
    host: 'localhost', // 主机名
    stats: 'errors-only', // 打包日志输出输出错误信息
    port: 8081,
    open: true
  },
}

配置打包命令

"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"

运行与打包

yarn dev
yarn build

2、TypeScript常用语法

2.1 基础类型

TypeScript 支持与 JavaScript 几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。

布尔值

最基本的数据类型就是简单的 true/false 值,在JavaScript 和 TypeScript 里叫做 boolean(其它语言中也一样)。

let isDone: boolean = false;
isDone = true;
// isDone = 2 // error

数字

和 JavaScript 一样,TypeScript 里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript 还支持 ECMAScript 2015中引入的二进制和八进制字面量。

let a1: number = 10 // 十进制
let a2: number = 0b1010  // 二进制
let a3: number = 0o12 // 八进制
let a4: number = 0xa // 十六进制

字符串

JavaScript 程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string 表示文本数据类型。 和 JavaScript 一样,可以使用双引号(")或单引号(')表示字符串。

let name:string = 'tom'
name = 'jack'
// name = 12 // error
let age:number = 12
const info = `My name is ${name}, I am ${age} years old!`

undefined 和 null

TypeScript 里,undefinednull 两者各自有自己的类型分别叫做 undefinednull。 它们的本身的类型用处不是很大:

let u: undefined = undefined
let n: null = null

默认情况下 nullundefined 是所有类型的子类型。 就是说你可以把 nullundefined 赋值给 number 类型的变量。

数组

TypeScript 像 JavaScript 一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[],表示由此类型元素组成的一个数组:

let list1: number[] = [1, 2, 3]

第二种方式是使用数组泛型,Array<元素类型>

let list2: Array<number> = [1, 2, 3]

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 stringnumber 类型的元组。

let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(t1[0].substring(1)) // OK
console.log(t1[1].substring(1)) // Error, 'number' 不存在 'substring' 方法

枚举

enum 类型是对 JavaScript 标准数据类型的一个补充。 使用枚举类型可以为一组数值赋予友好的名字

enum Color {
  Red,
  Green,
  Blue
}

// 枚举数值默认从0开始依次递增
// 根据特定的名称得到对应的枚举数值
let myColor: Color = Color.Green  // 0
console.log(myColor, Color.Red, Color.Blue)

默认情况下,从 0 开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1 开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为 2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2]

console.log(colorName)  // 'Green'

any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any 类型来标记这些变量:

let notSure: any = 4
notSure = 'maybe a string'
notSure = false // 也可以是个 boolean

在对现有代码进行改写的时候,any 类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。并且当你只知道一部分数据的类型时,any 类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, 'free']

list[1] = 100

void

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void

/* 表示没有任何类型, 一般用来说明函数的返回值不能是undefined和null之外的值 */
function fn(): void {
  console.log('fn()')
  // return undefined
  // return null
  // return 1 // error
}

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefinednull

let unusable: void = undefined

object

object 表示非原始类型,也就是除 numberstringboolean之外的类型。

使用 object 类型,就可以更好的表示像 Object.create 这样的 API。例如:

function fn2(obj:object):object {
  console.log('fn2()', obj)
  return {}
  // return undefined
  // return null
}
console.log(fn2(new String('abc')))
// console.log(fn2('abc') // error
console.log(fn2(String))

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种
需求1: 定义一个一个函数得到一个数字或字符串值的字符串形式值

function toString2(x: number | string) : string {
  return x.toString()
}

需求2: 定义一个一个函数得到一个数字或字符串值的长度

function getLength(x: number | string) {

  // return x.length // error

  if (x.length) { // error
    return x.length
  } else {
    return x.toString().length
  }
}

类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法, 另一个为 as 语法

/* 
类型断言(Type Assertion): 可以用来手动指定一个值的类型
语法:
    方式一: <类型>值
    方式二: 值 as 类型  tsx中只能用这种方式
*/

/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
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类型

/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定义变量时没有赋值, 推断为any类型 */
let b10  // any类型
b10 = 123
b10 = 'abc'

2.2 接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。我们使用接口(Interfaces)来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)

接口初探

需求: 创建人的对象, 需要对人的属性进行一定的约束

id是number类型, 必须有, 只读的
name是string类型, 必须有
age是number类型, 必须有
sex是string类型, 可以没有

下面通过一个简单示例来观察接口是如何工作的:

/* 
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型
接口: 是对象的状态(属性)和行为(方法)的抽象(描述)
接口类型的对象
    多了或者少了属性是不允许的
    可选属性: ?
    只读属性: readonly
*/

/* 
需求: 创建人的对象, 需要对人的属性进行一定的约束
  id是number类型, 必须有, 只读的
  name是string类型, 必须有
  age是number类型, 必须有
  sex是string类型, 可以没有
*/

// 定义人的接口
interface IPerson {
  id: number
  name: string
  age: number
  sex: string
}

const person1: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  sex: '男'
}

类型检查器会查看对象内部的属性是否与IPerson接口描述一致, 如果不一致就会提示类型错误。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。

interface IPerson {
  id: number
  name: string
  age: number
  sex?: string
}

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个 ? 符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。

const person2: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以没有
}

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly 来指定只读属性:

interface IPerson {
  readonly id: number
  name: string
  age: number
  sex?: string
}

一旦赋值后再也不能被改变了。

const person2: IPerson = {
  id: 2,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以没有
  // xxx: 12 // error 没有在接口中定义, 不能有
}
person2.id = 2 // error

readonly vs const

最简单判断该用 readonly 还是 const 的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用 readonly

函数类型

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

/* 
接口可以描述函数类型(参数的类型与返回的类型)
*/

interface SearchFunc {
  (source: string, subString: string): boolean
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

const mySearch: SearchFunc = function (source: string, sub: string): boolean {
  return source.search(sub) > -1
}

console.log(mySearch('abcd', 'bc'))

类类型

类实现接口

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约。

/* 
类类型: 实现接口
1. 一个类可以实现多个接口
2. 一个接口可以继承多个接口
*/

interface Alarm {
  alert(): any;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm {
  alert() {
      console.log('Car alert');
  }
}

一个类可以实现多个接口

class Car2 implements Alarm, Light {
  alert() {
    console.log('Car alert');
  }
  lightOn() {
    console.log('Car light on');
  }
  lightOff() {
    console.log('Car light off');
  }
}

接口继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface LightableAlarm extends Alarm, Light {

}

2.3 类

对于传统的 JavaScript 程序我们会使用函数基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员使用这些语法就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从 ECMAScript 2015,也就是 ES6 开始, JavaScript 程序员将能够使用基于类的面向对象的方式。 使用 TypeScript,我们允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下个 JavaScript 版本。

基本示例

下面看一个使用类的例子:

/* 
类的基本定义与使用
*/

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 里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

看下面的例子:

/* 
类的继承
*/

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()

下面我们来看个更加复杂的例子。

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()
/* 如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()

这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends 关键字创建了 Animal的两个子类:HorseSnake

与前一个例子的不同点是,派生类包含了一个构造函数,它 必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this 的属性之前,我们 一定要调用 super()。 这个是 TypeScript 强制执行的一条重要规则。

这个例子演示了如何在子类里可以重写父类的方法。Snake类和 Horse 类都创建了 run 方法,它们重写了从 Animal 继承来的 run 方法,使得 run 方法根据不同的类而具有不同的功能。注意,即使 tom 被声明为 Animal 类型,但因为它的值是 Horse,调用 tom.run(34) 时,它会调用 Horse 里重写的方法。

sliding...
sn run 5m
dashing...
ho run 50m

公共,私有与受保护的修饰符

默认为 public

在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public 来做修饰;例如,C# 要求必须明确地使用 public 指定成员是可见的。 在 TypeScript 里,成员都默认为 public

你也可以明确的将一个成员标记成 public。 我们可以用下面的方式来重写上面的 Animal 类:

理解 private

当成员被标记成 private 时,它就不能在声明它的类的外部访问。

理解 protected

protected 修饰符与 private 修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问。例如:

/* 
访问修饰符: 用来描述类内部的属性/方法的可访问性
  public: 默认值, 公开的外部也可以访问
  private: 只能类内部可以访问
  protected: 类内部和子类可以访问
*/

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) // 子类能看到父类中受保护的成员
    // console.log(this.age) //  子类看不到父类中私有的成员

    super.run(distance)
  }
}

console.log(new Person('abc').name) // 公开的可见
// console.log(new Person('abc').sex) // 受保护的不可见
// console.log(new Person('abc').age) //  私有的不可见

readonly 修饰符

你可以使用 readonly 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Person {
  readonly name: string = 'abc'
  constructor(name: string) {
    this.name = name
  }
}

let john = new Person('John')
// john.name = 'peter' // error

参数属性

在上面的例子中,我们必须在 Person 类里定义一个只读成员 name 和一个参数为 name 的构造函数,并且立刻将 name 的值赋给 this.name,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person 类的修改版,使用了参数属性:

class Person2 {
  constructor(readonly name: string) {
  }
}

const p = new Person2('jack')
console.log(p.name)

注意看我们是如何舍弃参数 name,仅在构造函数里使用 readonly name: string 参数来创建和初始化 name 成员。 我们把声明和赋值合并至一处。

参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 publicprotected 来说也是一样。

存取器

TypeScript 支持通过 getters/setters 来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用 getset。 首先,我们从一个没有使用存取器的例子开始。

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 来访问静态属性。

/* 
静态属性, 是类对象的属性
非静态属性, 是类的实例对象的属性
*/

class Person {
  name1: string = 'A'
  static name2: string = 'B'
}

console.log(Person.name2)
console.log(new Person().name1)

抽象类

抽象类做为其它派生类的基类使用。 它们不能被实例化。不同于接口,抽象类可以包含成员的实现细节。 abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

/* 
抽象类
  不能创建实例对象, 只有实现类才能创建实例
  可以包含未实现的抽象方法
*/

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()

2.4 函数

函数是 JavaScript 应用程序的基础,它帮助你实现抽象层,模拟类,信息隐藏和模块。在 TypeScript 里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义行为的地方。TypeScript 为 JavaScript 函数添加了额外的功能,让我们可以更容易地使用。

基本示例

和 JavaScript 一样,TypeScript 函数可以创建有名字的函数和匿名函数。你可以随意选择适合应用程序的方式,不论是定义一系列 API 函数还是只使用一次的函数。

通过下面的例子可以迅速回想起这两种 JavaScript 中的函数:

// 命名函数
function add(x, y) {
  return x + y
}

// 匿名函数
let myAdd = function(x, y) { 
  return x + y;
}

函数类型

为函数定义类型

让我们为上面那个函数添加类型:

function add(x: number, y: number): number {
  return x + y
}

let myAdd = function(x: number, y: number): number { 
  return x + y
}

我们可以给每个参数添加类型之后再为函数本身添加返回值类型。TypeScript 能够根据返回语句自动推断出返回值类型。

书写完整函数类型

现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。

let myAdd2: (x: number, y: number) => number = 
function(x: number, y: number): number {
  return x + y
}

可选参数和默认参数

TypeScript 里的每个函数参数都是必须的。 这不是指不能传递 nullundefined 作为参数,而是说编译器检查用户是否为每个参数都传入了值。编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。

JavaScript 里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是 undefined。 在TypeScript 里我们可以在参数名旁使用 ? 实现可选参数的功能。 比如,我们想让 lastName 是可选的:

在 TypeScript 里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是 undefined 时。 它们叫做有默认初始化值的参数。 让我们修改上例,把firstName 的默认值设置为 "A"

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 里,你可以把所有参数收集到一个变量里:
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...)后面给定的名字,你可以在函数体内使用这个数组。

function info(x: string, ...args: string[]) {
  console.log(x, args)
}
info('abc', 'c', 'b', 'a')

函数重载

函数重载: 函数名相同, 而形参不同的多个函数
在JS中, 由于弱类型的特点和形参与实参可以不匹配, 是没有函数重载这一说的
但在TS中, 与其它面向对象的语言(如Java)就存在此语法

/* 
函数重载: 函数名相同, 而形参不同的多个函数
需求: 我们有一个add函数,它可以接收2个string类型的参数进行拼接,也可以接收2个number类型的参数进行相加 
*/

// 重载函数声明
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'))
// console.log(add(1, 'a')) // error

2.5 泛型

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

引入

下面创建一个函数, 实现功能: 根据指定的数量 count 和数据 value , 创建一个包含 countvalue 的数组
不用泛型的话,这个函数可能是下面这样:

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(''))

使用函数泛型

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())
// console.log(arr3[0].split('')) // error
const arr4 = createArray2<string>('aa', 3)
console.log(arr4[0].split(''))
// console.log(arr4[0].toFixed()) // error

多个泛型参数的函数

一个函数可以定义多个泛型参数

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())

泛型接口

在定义接口时, 为接口中的属性或方法定义泛型类型
在使用接口时, 再指定具体的泛型类型

interface IbaseCRUD <T> {
  data: T[]
  add: (t: T) => void
  getById: (id: number) => T
}

class User {
  id?: number; //id主键自增
  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)

泛型类

在定义类时, 为类中的属性或方法定义泛型类型
在创建类的实例时, 再指定特定的泛型类型

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 属性, 会报错, 因为这个泛型根本就不知道它有这个属性

// 没有泛型约束
function fn <T>(x: T): void {
  // console.log(x.length)  // error
}

我们可以使用泛型约束来实现

interface Lengthwise {
  length: number;
}

// 指定泛型约束
function fn2 <T extends Lengthwise>(x: T): void {
  console.log(x.length)
}

我们需要传入符合约束类型的值,必须包含必须 length 属性:

fn2('abc')
// fn2(123) // error  number没有length属性

2.6 其它

声明文件

当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能

什么是声明语句

假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script> 标签引入 jQuery,然后就可以使用全局变量 $jQuery 了。

但是在 ts 中,编译器并不知道 $ 或 jQuery 是什么东西

/* 
当使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
声明语句: 如果需要ts对新的语法进行检查, 需要要加载了对应的类型说明代码
  declare var jQuery: (selector: string) => any;
声明文件: 把声明语句放到一个单独的文件(jQuery.d.ts)中, ts会自动解析到项目中所有声明文件
下载声明文件: npm install @types/jquery --save-dev
*/

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

这时,我们需要使用 declare var 来定义它的类型

declare var jQuery: (selector: string) => any;

jQuery('#foo');

declare var 并没有真的定义一个变量,只是定义了全局变量 jQuery 的类型,仅仅会用于编译时的检查,在编译结果中会被删除。它编译结果是:

jQuery('#foo');

一般声明文件都会单独写成一个 xxx.d.ts 文件

创建 01_jQuery.d.ts, 将声明语句定义其中, TS编译器会扫描并加载项目中所有的TS声明文件

declare var jQuery: (selector: string) => any;

很多的第三方库都定义了对应的声明文件库, 库文件名一般为 @types/xxx, 可以在 https://www.npmjs.com/package/package 进行搜索

有的第三库在下载时就会自动下载对应的声明文件库(比如: webpack),有的可能需要单独下载(比如jQuery/react)

内置对象

JavaScript 中有很多内置对象,它们可以直接在 TypeScript 中当做定义好了的类型。

内置对象是指根据标准在全局作用域(Global)上存在的对象。这里的标准是指 ECMAScript 和其他环境(比如 DOM)的标准。

  1. ECMAScript 的内置对象

Boolean
Number
String
Date
RegExp
Error

/* 1. ECMAScript 的内置对象 */
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
// let bb: boolean = new Boolean(2)  // error
  1. BOM 和 DOM 的内置对象

Window
Document
HTMLElement
DocumentFragment
Event
NodeList

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()

3、认识Vue3

3.1 了解相关信息

  • Vue.js 3.0 “One Piece” 正式版在今年9月份发布
  • 2年多开发, 100+位贡献者, 2600+次提交, 600+次PR
  • Vue3支持vue2的大多数特性
  • 更好的支持Typescript

3.2 性能提升:

  • 打包大小减少41%
  • 初次渲染快55%, 更新渲染快133%
  • 内存减少54%
  • 使用Proxy代替defineProperty实现数据响应式
  • 重写虚拟DOM的实现和Tree-Shaking

3.3 新增特性

  • Composition (组合) API

  • setup

    • ref 和 reactive
    • computed 和 watch
    • 新的生命周期函数
    • provide与inject
  • 新组件

    • Fragment - 文档碎片
    • Teleport - 瞬移组件的位置
    • Suspense - 异步加载组件的loading界面
  • 其它API更新

    • 全局API的修改
    • 将原来的全局API转移到应用对象
    • 模板语法变化

4、创建vue3项目

4.1 使用 vue-cli 创建

文档: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

## 安装或者升级
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? - 直接回车

4.2 使用 vite 创建

  • 文档: https://v3.cn.vuejs.org/guide/installation.html

  • vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,

  • 它做到了***本地快速开发启动***, 在生产环境下基于 Rollup 打包。

    • 快速的冷启动,不需要等待打包操作;
    • 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
    • 真正的按需编译,不再等待整个应用编译完成,这是一个巨大的改变。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

5、Composition API(常用部分)

文档:

https://composition-api.vuejs.org/zh/api.html

5.1 setup

  • 新的option, 所有的组合API函数都在此使用, 只在初始化时执行一次
  • 函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用

5.2 ref

  • 作用: 定义一个数据的响应式
  • 语法: const xxx = ref(initValue):
    • 创建一个包含响应式数据的引用(reference)对象
    • js中操作数据: xxx.value
    • 模板中操作数据: 不需要.value
  • 一般用来定义一个基本类型的响应式数据
<template>
  <h2>{{count}}</h2>
  <hr>
  <button @click="update">更新</button>
</template>

<script>
import {
  ref
} from 'vue'
export default {

  /* 在Vue3中依然可以使用data和methods配置, 但建议使用其新语法实现 */
  // data () {
  //   return {
  //     count: 0
  //   }
  // },
  // methods: {
  //   update () {
  //     this.count++
  //   }
  // }

  /* 使用vue3的composition API */
  setup () {

    // 定义响应式数据 ref对象
    const count = ref(1)
    console.log(count)

    // 更新响应式数据的函数
    function update () {
      // alert('update')
      count.value = count.value + 1
    }

    return {
      count,
      update
    }
  }
}
</script>

5.3 reactive

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
<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>

5.4 比较Vue2与Vue3的响应式(重要)

5.4.1 vue2的响应式

  • 核心:
    • 对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
    • 数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
    get () {}, 
    set () {}
})
  • 问题
    • 对象直接新添加的属性或删除已有属性, 界面不会自动更新
    • 直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}

5.4.2 Vue3的响应式

  • 核心:
    • 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
    • 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
    • 文档:
      • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
      • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
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
    };

    /* 
    proxyUser是代理对象, user是被代理对象
    后面所有的操作都是通过代理对象来操作被代理对象内部属性
    */
    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); // (2)
      },

      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.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
<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 {
      // n: 1
    }
  },

  beforeCreate () {
    console.log('beforeCreate', this)
  },

  methods: {
    // update () {
    //   this.n++
    //   this.m++
    // }
  },

  // setup (props, context) {
  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 () {
      // console.log('--', this)
      // this.n += 2 
      // this.m += 2

      m.value += 2
      n.value += 2

      // 分发自定义事件
      emit('fn', '++')
    }

    return {
      m,
      n,
      update,
    }
  },
})
</script>

5.6 reactive与ref-细节

  • 是Vue3的 composition API中2个最重要的响应式API
  • ref用来处理基本类型数据, reactive用来处理对象(递归深度响应式)
  • 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象
  • ref内部: 通过给value属性添加getter/setter来实现对数据的劫持
  • reactive内部: 通过使用Proxy来实现对对象内部所有数据的劫持, 并通过Reflect操作对象内部数据
  • ref的数据操作: 在js中要.value, 在模板中不需要(内部解析模板时会自动添加.value)
<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'}})

    // 使用ref处理对象  ==> 对象会被自动reactive为proxy对象
    const m3 = ref({a1: 2, a2: {a3: 'abc'}})
    console.log(m1, m2, m3)
    console.log(m3.value.a2) // 也是一个proxy对象

    function update() {
      m1.value += '--'
      m2.x += 1
      m2.y.z += '++'

      m3.value = {a1: 3, a2: {a3: 'abc---'}}
      m3.value.a2.a3 += '==' // reactive对对象进行了深度数据劫持
      console.log(m3.value.a2)
    }

    return {
      m1,
      m2,
      m3,
      update
    }
  }
}
</script>

5.7 计算属性与监视

  • computed函数:

    • 与computed配置功能一致
    • 只有getter
    • 有getter和setter
  • watch函数

    • 与watch配置功能一致
    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
    • 默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
    • 通过配置deep为true, 来指定深度监视
  • watchEffect函数

    • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
    • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
    • 监视数据发生变化时回调
<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">
/*
计算属性与监视
1. computed函数: 
  与computed配置功能一致
  只有getter
  有getter和setter
2. watch函数
  与watch配置功能一致
  监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
  默认初始时不执行回调, 但可以通过配置immediate为true, 来指定初始时立即执行第一次
  通过配置deep为true, 来指定深度监视
3. watchEffect函数
  不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
  默认初始时就会执行第一次, 从而可以收集需要监视的数据
  监视数据发生变化时回调
*/

import {
  reactive,
  ref,
  computed,
  watch,
  watchEffect
} from 'vue'

export default {

  setup () {
    const user = reactive({
      firstName: 'A',
      lastName: 'B'
    })

    // 只有getter的计算属性
    const fullName1 = computed(() => {
      console.log('fullName1')
      return user.firstName + '-' + user.lastName
    })

    // 有getter与setter的计算属性
    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('')

    /* 
    watchEffect: 监视所有回调中使用的数据
    */
    /* 
    watchEffect(() => {
      console.log('watchEffect')
      fullName3.value = user.firstName + '-' + user.lastName
    }) 
    */

    /* 
    使用watch的2个特性:
      深度监视
      初始化立即执行
    */
    watch(user, () => {
      fullName3.value = user.firstName + '-' + user.lastName
    }, {
      immediate: true,  // 是否初始化立即执行一次, 默认是false
      deep: true, // 是否是深度监视, 默认是false
    })

    /* 
    watch一个数据
      默认在数据发生改变时执行回调
    */
    watch(fullName3, (value) => {
      console.log('watch')
      const names = value.split('-')
      user.firstName = names[0]
      user.lastName = names[1]
    })

    /* 
    watch多个数据: 
      使用数组来指定
      如果是ref对象, 直接指定
      如果是reactive对象中的属性,  必须通过函数来指定
    */
    watch([() => user.firstName, () => user.lastName, fullName3], (values) => {
      console.log('监视多个数据', values)
    })

    return {
      user,
      fullName1,
      fullName2,
      fullName3
    }
  }
}
</script>

5.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
<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>

5.9 自定义hook函数

  • 使用Vue3的组合API封装的可复用的功能函数

  • 自定义hook的作用类似于vue2中的mixin技术

  • 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂

  • 需求1: 收集用户鼠标点击的页面坐标

    hooks/useMousePosition.ts

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"
/* 
在组件中引入并使用自定义hook
自定义hook的作用类似于vue2中的mixin技术
自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂
*/
import useMousePosition from './hooks/useMousePosition'

export default {
  setup() {

    const {x, y} = useMousePosition()

    return {
      x,
      y,
    }
  }
}
</script>
  • 利用TS泛型强化类型检查

  • 需求2: 封装发ajax请求的hook函数

    hooks/useRequest.ts

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>
  <!-- <ul v-else>
    <li>id: {{result.id}}</li>
    <li>name: {{result.name}}</li>
    <li>distance: {{result.distance}}</li>
  </ul> -->

  <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>
  <!-- <img v-if="result" :src="result[0].url" alt=""> -->
</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<AddressResult>('/data/address.json')
    const {loading, result, errorMsg} = useRequest<ProductResult[]>('/data/products.json')

    watch(result, () => {
      if (result.value) {
        console.log(result.value.length) // 有提示
      }
    })

    return {
      loading,
      result, 
      errorMsg
    }
  }
}
</script>

5.10 toRefs

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref

应用: 当从合成函数返回响应式对象时,toRefs 非常有用,这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用

问题: reactive 对象取出的所有属性值都是非响应式的

解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性

<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, toRefs } from 'vue'
/*
toRefs:
  将响应式对象中所有属性包装为ref对象, 并返回包含这些ref对象的普通对象
  应用: 当从合成函数返回响应式对象时,toRefs 非常有用,
        这样消费组件就可以在不丢失响应式的情况下对返回的对象进行分解使用
*/
export default {

  setup () {

    const state = reactive({
      foo: 'a',
      bar: 'b',
    })

    const stateAsRefs = toRefs(state)

    setTimeout(() => {
      state.foo += '++'
      state.bar += '++'
    }, 2000);

    const {foo2, bar2} = useReatureX()

    return {
      // ...state,
      ...stateAsRefs,
      foo2, 
      bar2
    }
  },
}

function useReatureX() {
  const state = reactive({
    foo2: 'a',
    bar2: 'b',
  })

  setTimeout(() => {
    state.foo2 += '++'
    state.bar2 += '++'
  }, 2000);

  return toRefs(state)
}

</script>

5.11 ref获取元素

利用ref函数获取组件中的标签元素

功能需求: 让输入框自动获取焦点

<template>
  <h2>App</h2>
  <input type="text">---
  <input type="text" ref="inputRef">
</template>

<script lang="ts">
import { onMounted, ref } from 'vue'
/* 
ref获取元素: 利用ref函数获取组件中的标签元素
功能需求: 让输入框自动获取焦点
*/
export default {
  setup() {
    const inputRef = ref<HTMLElement|null>(null)

    onMounted(() => {
      inputRef.value && inputRef.value.focus()
    })

    return {
      inputRef
    }
  },
}
</script>

6、Composition API(其它部分)

6.1 shallowReactive 与 shallowRef

  • shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)

  • shallowRef: 只处理了value的响应式, 不进行对象的reactive处理

  • 什么时候用浅响应式呢?

    • 一般情况下使用ref和reactive即可
    • 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    • 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
<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'
/* 
shallowReactive与shallowRef
  shallowReactive: 只处理了对象内最外层属性的响应式(也就是浅响应式)
  shallowRef: 只处理了value的响应式, 不进行对象的reactive处理
总结:
  reactive与ref实现的是深度响应式, 而shallowReactive与shallowRef是浅响应式
  什么时候用浅响应式呢?
    一般情况下使用ref和reactive即可,
    如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
*/

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 = () => {
      // m1.b.c += 1
      // m2.b.c += 1

      // m3.value.a += 1
      m4.value.a += 1
    }

    return {
      m1,
      m2,
      m3,
      m4,
      update,
    }
  }
}
</script>

6.2 readonly 与 shallowReadonly

  • readonly:
    • 深度只读数据
    • 获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。
    • 只读代理是深层的:访问的任何嵌套 property 也是只读的。
  • shallowReadonly
    • 浅只读数据
    • 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换
  • 应用场景:
    • 在某些特定情况下, 我们可能不希望对数据进行更新的操作, 那就可以包装生成一个只读代理对象来读取数据, 而不能修改或删除
<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>

6.3 toRaw 与 markRaw

  • toRaw
    • 返回由 reactivereadonly 方法转换成响应式代理的普通对象。
    • 这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发界面更新。
  • markRaw
    • 标记一个对象,使其永远不会转换为代理。返回对象本身
    • 应用场景:
      • 有些值不应被设置为响应式的,例如复杂的第三方类实例或 Vue 组件对象。
      • 当渲染具有不可变数据源的大列表时,跳过代理转换可以提高性能。
<template>
  <h2>{{state}}</h2>
  <button @click="testToRaw">测试toRaw</button>
  <button @click="testMarkRaw">测试markRaw</button>
</template>

<script lang="ts">
/* 
toRaw: 得到reactive代理对象的目标数据对象
*/
import {
  markRaw,
  reactive, toRaw,
} from 'vue'
export default {
  setup () {
    const state = reactive<any>({
      name: 'tom',
      age: 25,
    })

    const testToRaw = () => {
      const user = toRaw(state)
      user.age++  // 界面不会更新

    }

    const testMarkRaw = () => {
      const likes = ['a', 'b']
      // state.likes = likes
      state.likes = markRaw(likes) // likes数组就不再是响应式的了
      setTimeout(() => {
        state.likes[0] += '--'
      }, 1000)
    }

    return {
      state,
      testToRaw,
      testMarkRaw,
    }
  }
}
</script>

6.4 toRef

  • 为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  • 区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  • 应用: 当要将 某个prop 的 ref 传递给复合函数时,toRef 很有用
<template>
  <h2>App</h2>
  <p>{{state}}</p>
  <p>{{foo}}</p>
  <p>{{foo2}}</p>

  <button @click="update">更新</button>

  <Child :foo="foo"/>
</template>

<script lang="ts">
/*
toRef:
  为源响应式对象上的某个属性创建一个 ref对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  区别ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  应用: 当要将某个 prop 的 ref 传递给复合函数时,toRef 很有用
*/

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++
      // foo.value++
      // foo2.value++  // foo和state中的数据不会更新
    }

    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>

6.5 customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
  • 需求: 使用 customRef 实现 debounce 的示例
<template>
  <h2>App</h2>
  <input v-model="keyword" placeholder="搜索关键字"/>
  <p>{{keyword}}</p>
</template>

<script lang="ts">
/*
customRef:
  创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

需求: 
  使用 customRef 实现 debounce 的示例
*/

import {
  ref,
  customRef
} from 'vue'

export default {

  setup () {
    const keyword = useDebouncedRef('', 500)
    console.log(keyword)
    return {
      keyword
    }
  },
}

/* 
实现函数防抖的自定义ref
*/
function useDebouncedRef<T>(value: T, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        // 告诉Vue追踪数据
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          // 告诉Vue去触发界面更新
          trigger()
        }, delay)
      }
    }
  })
}

</script>

6.6 provide 与 inject

  • provideinject提供依赖注入,功能类似 2.x 的provide/inject

  • 实现跨层级组件(祖孙)间通信

<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'
/* 
- provide` 和 `inject` 提供依赖注入,功能类似 2.x 的 `provide/inject
- 实现跨层级组件(祖孙)间通信
*/

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>

6.7 响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

7、手写组合API

7.1 shallowReactive 与 reactive

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
  },
}

/* 
自定义shallowReactive
*/
function shallowReactive(obj) {
  return new Proxy(obj, reactiveHandler)
}

/* 
自定义reactive
*/
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
}


/* 测试自定义shallowReactive */
const proxy = shallowReactive({
  a: {
    b: 3
  }
})

proxy.a = {b: 4} // 劫持到了
proxy.a.b = 5 // 没有劫持到


/* 测试自定义reactive */
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

7.2 shallowRef 与 ref

/*
自定义shallowRef
*/
function shallowRef(target) {
  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    }
  }

  return result
}

/* 
自定义ref
*/
function ref(target) {
  if (target && typeof target==='object') {
    target = reactive(target)
  }

  const result = {
    _value: target, // 用来保存数据的内部属性
    _is_ref: true, // 用来标识是ref对象
    get value () {
      return this._value
    },
    set value (val) {
      this._value = val
      console.log('set value 数据已更新, 去更新界面')
    }
  }

  return result
}

/* 测试自定义shallowRef */
const ref3 = shallowRef({
  a: 'abc',
})
ref3.value = 'xxx'
ref3.value.a = 'yyy'


/* 测试自定义ref */
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)

7.3 shallowReadonly 与 readonly

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
  },
}

/* 
自定义shallowReadonly
*/
function shallowReadonly(obj) {
  return new Proxy(obj, readonlyHandler)
}

/* 
自定义readonly
*/
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
}

/* 测试自定义readonly */
/* 测试自定义shallowReadonly */
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

7.4 isRef, isReactive 与 isReadonly

/* 
判断是否是ref对象
*/
function isRef(obj) {
  return obj && obj._is_ref
}

/* 
判断是否是reactive对象
*/
function isReactive(obj) {
  return obj && obj._is_reactive
}

/* 
判断是否是readonly对象
*/
function isReadonly(obj) {
  return obj && obj._is_readonly
}

/* 
是否是reactive或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({})))

8、Composition API VS Option API

8.1 Option API的问题

  • 在传统的Vue OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 ,滚动条反复上下移动

8.2 使用Compisition API

我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起

9、新组件

9.1 Fragment(片断)

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用
<template>
    <h2>aaaa</h2>
    <h2>aaaa</h2>
</template>

9.2 Teleport(瞬移)

  • Teleport 提供了一种干净的方法, 让组件的html在父组件界面外的特定标签(很可能是body)下插入显示

ModalButton.vue

<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

<template>
  <h2>App</h2>
  <modal-button></modal-button>
</template>

<script lang="ts">
import ModalButton from './ModalButton.vue'

export default {
  setup() {
    return {
    }
  },

  components: {
    ModalButton
  }
}
</script>

9.3 Suspense(不确定的)

  • 它们允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
<template>
  <Suspense>
    <template v-slot:default>
      <AsyncComp/>
      <!-- <AsyncAddress/> -->
    </template>

    <template v-slot:fallback>
      <h1>LOADING...</h1>
    </template>
  </Suspense>
</template>

<script lang="ts">
/* 
异步组件 + Suspense组件
*/
// import AsyncComp from './AsyncComp.vue'
import AsyncAddress from './AsyncAddress.vue'
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
export default {
  setup() {
    return {
     
    }
  },

  components: {
    AsyncComp,
    AsyncAddress
  }
}
</script>
  • AsyncComp.vue
<template>
  <h2>AsyncComp22</h2>
  <p>{{msg}}</p>
</template>

<script lang="ts">

export default {
  name: 'AsyncComp',
  setup () {
    // return new Promise((resolve, reject) => {
    //   setTimeout(() => {
    //     resolve({
    //       msg: 'abc'
    //     })
    //   }, 2000)
    // })
    return {
      msg: 'abc'
    }
  }
}
</script>
  • AsyncAddress.vue
<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>

10、其他新的API

10.1 全新的全局API

  • createApp()
  • defineProperty()
  • defineAsyncComponent()
  • nextTick()

10.2 将原来的全局API转移到应用对象

  • app.component()
  • app.config()
  • app.directive()
  • app.mount()
  • app.unmount()
  • app.use()

10.3 模板语法变化

  • v-model的本质变化
    • prop:value -> modelValue;
    • event:input -> update:modelValue;
  • .sync修改符已移除, 由v-model代替
  • v-if优先v-for解析

11、使用VuePress搭建在线文档网站

11.1 在线文档

VuePress官方在线文档

11.2 搭建基本环境

# 将 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

11.3 配置ts教程文档

  1. 整体结构
|-- 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
  1. docs/.vuepress/config.js
// 注意: 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',
        ]
      },
    ]
  }
}
  1. docs/README.md
---
#首页
home: true  
# 图标
heroImage: /ts-logo.png
# 按钮文本
actionText: 开始学习 →
# 按钮点击跳转路径
actionLink: /chapter1/01_初识TS
---
  1. package.json
"scripts": {
  "doc:dev": "vuepress dev docs",
  "doc:build": "vuepress build docs",
  "doc:deploy": "gh-pages -d docs/dist"
}

11.4 发布到gitpage

  1. 使用git管理当前项目

  2. 将打包的项目推送到gitpage

# 下载工具包
yarn add -D gh-pages
# 执行打包命令
yarn doc:build
# 执行部署命令
yarn doc:deploy
评论 7 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:像素格子 设计师:CSDN官方博客 返回首页

打赏作者

淮城一只猫

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值