Skip to content

ts 基础语法

基础类型

typescript
// ts 中变量一开始定义的是什么类型,那么后续赋值时只能用这个类型的数据
let isDone: boolean = false
let a1: number = 10
let name: string = 'tom'
let u: undefined = undefined
let n: null = null
// 默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null 和 undefined 赋值给 number 类型的变量。如下:
let a1: number = undefined
let name: string = null

数组

typescript
// 定义数组的两种写法,这两种写法规定数组中的元素必须为前面定义好的数据类型
let list1: number[] = [1, 2, 3]
let list2: Array<number> = [1, 2, 3]

元组

typescript
// 这种写法规定了数组的长度以及每项元素的数据类型
let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

let t1: [string, number] = ['hello', 10]
console.log(t1[0].split(''))
console.log(t1[1]toFixed(2))

枚举

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

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

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

typescript
enum Color {
  Red = 1,
  Green,
  Blue
}
let c: Color = Color.Green
console.log(c, Color.Red, Color.Blue) //1,2,3

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

typescript
enum Color {
  Red = 1,
  Green = 2,
  Blue = 4
}
let c: Color = Color.Green
console.log(c, Color.Red, Color.Blue) //1,2,4

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

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

console.log(colorName) // 'Green'

any

any 可以很方便的存储任意其它类型的数据

typescript
let notSure: any = 4
notSure = 'maybe a string'
notSure = false // 也可以是个 boolean
// 数组里也能使用
let list: any[] = [1, true, 'free']
list[1] = 100

使用注意点:

typescript
let list: any[] = [1, true, 'free']
console.log(list[0].split('')) // 这种情况下可以编译通过,但实际运行会报错

unknown

typescript
let d // 不对变量设置类型时默认为 any
let s: string
// s = d // any 可以赋值给其他类型的值,但是不建议这样做
let e: unknown // 和 any 很相似可以赋值任意类型
e = 10
e = 'hello'
e = true
// unknown 实际上就是一个安全的 any,unknown 类型的变量不能直接赋值给其他变量
s = e // 和 any 不同这时会报错,不能将 unknown 类型的值赋值给其他类型的变量
// 这时必须这样写才能赋值
if (typeof e === 'string') {
  s = e
}
// 或者使用类型断言
s = e as string // 另一种写法:s = <string>e

void

typescript
// 和 any 相反表示没有任何类型,当一个函数没有返回值时,你通常会见到其返回值类型是 void:
// 表示没有任何类型, 一般用来说明函数的返回值不能是undefined和null之外的值
function fn(): void {
  // 函数不写任何类型,默认就是 void 类型
  console.log('fn()')
  // return undefined
  // return null
  // return 1 // error
}

object

typescript
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))
// 定义多个属性
// [propName:string]:string 表示对象后面的任意个属性且属性值必须为字符串
let b:{name:string,age?number,[propName:string]:string}
c = {name:'猪八戒',gender:'男'}

类型别名

typescript
// 类型别名
type mytype = 1 | 2 | 3 | 4 | 5
let k: mytype
// & 表示且同时满足
let j: { name: string } & { age: number } // 这里表示j有两个属性一个是 string 类型的一个是 number类型
j = { name: '孙悟空', age: 19 }

联合类型

typescript
function toString2(x: number | string): string {
  return x.toString()
}
console.log(toString2(1)) // 此时参数可以为数字或者字符串

类型断言

typescript
// 定义一个一个函数得到一个数字或字符串值的长度
function getLength(x: number | string) {
  if (x.length) {
    //这时编译器不清楚 x 是什么类型,所以认为调用 length 出错
    // error
    return x.length
  } else {
    return x.toString().length
  }
}
//上面这种写法编译器会报错,这时就需要类型断言

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

typescript
function getLength(x: number | string) {
  if ((<string>x).length) {
    // 类型断言定义方式1 <类型>变量名
    return (x as string).length // 类型断言定义方式2 变量名 as 类型
  } else {
    return x.toString().length
  }
}
console.log(getLength('abcd'), getLength(1234))
ts
// 页面上有一个 id 为 link 的 a 标签
const alink = document.getElementById('link')
console.log(alink.href) // 这时会提示没有 href 属性

// 这时可以通过类型断言解决,这种写法在 react 会有冲突
const alink = <HTMLAnchorElement>document.getElementById('link')
console.log(alink.href) // 这时访问 href 就没问题了

// 建议使用这种写法
const alink = document.getElementById('link') as HTMLAnchorElement
console.log(alink.href) // 这时访问 href 就没问题了

类型推断

类型推断: TS 会在没有明确的指定类型的时候推测出一个类型 有下面 2 种情况: 1. 定义变量时赋值了, 推断为对应的类型. 2. 定义变量时没有赋值, 推断为 any 类型

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

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

接口

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

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

// 定义人的接口
interface IPerson {
  readonly id: number // readonly 可以用来定义只读数据
  name: string
  age: number
  sex?: string // 属性名? 表示这个属性可选(可有可无)
}

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

readonly vs const

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

函数类型

typescript
const f: (s1: string, s2: number) => string = function (s1, s3) {
  return s1 + s3
}

类类型

一个类可以实现多个接口

接口继承接口

接口继承使用 extends 继承

通过接口约束类

typescript
interface myinter {
  name: string
  say(): void
}
// 定义类时可以使类去实现一个接口
class myclass implements myinter {
  name: string
  constructor(name: string) {
    this.name = name
  }
  say() {
    console.log('...')
  }
}

交叉类型

继承和交叉类型对于属性名冲突时不同的处理方式

interface 和 type 的区别

不同点

interface 用于描述数据结构,type 用于描述类型

type 可以声明基本类型、联合类型、元组

type 可以声明合并(如果存在两个名称相同的接口最终这个接口的类型是两个的合集),如果是 interface 会报错

相同点

都可以用于描述对象或函数

都允许拓展,interface 可以 extends type,type 也可以 extends interface

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

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

继承

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) // 可以调用从父中继承得到的方法

类型兼容

注意

接口、类兼容性

属性多的可以赋值给属性少的,反之则不行

函数兼容性

注意:参数少的可以赋值给参数多的,反之则不行

返回值兼容

返回值多的可以赋值给少的,返回值一样可以互相赋值

实现接口(类约束)

typescript
// 用接口约束类需要使用 implements

多态

修饰符

typescript
/*
访问修饰符: 用来描述类内部的属性/方法的可访问性
  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 关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

typescript
class Person {
  readonly name: string = 'abc'
  constructor(name: string) {
    this.name = name
  }
  // readonly 只能用来修饰属性不能用来修饰函数
  // readonly seyHi(){
  // }
}

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

构造函数中的 name 参数只要使用了修饰符(readonly、public、private、protected ),那么 Person 中就有了一个 name 成员

存取器

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

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

静态属性

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

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

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

抽象类

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

abstract class Animal {
  abstract cry() // 用 abstract 修饰的方法不能有逻辑代码,且子类必须实现该方法

  run() {
    console.log('run()')
  }
}

class Dog extends Animal {
  cry() {
    console.log(' Dog cry()')
  }
}

const dog = new Dog()
dog.cry()
dog.run()

函数

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

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

可选参数和默认值

typescript
// 定义函数类型
let d = (a: string, b: string) => number
d = function (n1, n2) {
  return n1 + n2
}
// firstName: string = 'A',表示默认参数,lastName?: string 表示可选参数
function buildName(firstName: string = 'A', lastName?: string): string {
  if (lastName) {
    return firstName + '-' + lastName
  } else {
    return firstName
  }
}

剩余参数

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

函数重载

函数名相同,函数参数和个数不同

typescript
/*
函数重载: 函数名相同, 而形参不同的多个函数
需求: 我们有一个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,如果在上面没有定义函数重载则会返回 undefined 而不会提示错误

泛型

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

typescript
// 根据指定的数量 count 和数据 value , 创建一个包含 count 个 value 的数组 不用泛型的话,这个函数可能是下面这样:
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('')) // 这里写 toFixed 和 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

多个泛型参数的函数

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

泛型接口

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

typescript
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
  }
}
// implements 使用接口
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)

泛型类

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

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

泛型约束

typescript
// 如果我们直接对一个泛型参数取 length 属性, 会报错, 因为这个泛型根本就不知道它有这个属性
// 没有泛型约束
function fn<T>(x: T): void {
  // console.log(x.length)  // error
}
interface Lengthwise {
  length: number
}

// 指定泛型约束 指定泛型 T 中必须有 length 属性,否则报错
function fn2<T extends Lengthwise>(x: T): void {
  console.log(x.length)
}
fn2('abc')
// fn2(123) // error  number没有length属性

keyof

typeof

泛型工具类型

Partial

Partial 可以将一个已有的类型转换为一个属性都是可选的新类型

Readonly

Readonly 可以将一个已有的类型转换为一个所有属性都只读的新类型

Pick

Pick 可以从一个已有的类型中抽离一个或多个属性组成一个新类型

Record

Record 可以创建一个具有指定键和具体类型的值的类型

Exclude

根据现有的两个类型返回这两个类型的差集组成一个新类型(差集)

typescript
type A = number | string | boolean
type B = number | boolean
type foo = Exclude<A, B>
// 相当于
type foo = string

Extract

和 Exclude 相反(取交集)

typescript
type A = number | string | boolean
type B = number | boolean
type foo = Exclude<A, B>
// 相当于
type foo = number | boolean

Omit

根据现有类型排除指定属性之外将剩余所有组成一个新的类型

typescript
// Omit 只能用于对象类型,不能用于联合类型
type foo = {
  name: string
  age: number
}
type Bar = Omit<foo, 'age'>
// 相当于
type Bar = {
  name: string
}

Exclude 和 Omit 区别

typescript
// Exclude 一般用于联合类型,Omit 一般用于复杂对象类型
// Extract 一般用于联合类型,Pick 一般用于复杂对象类型

总结

typescript
// Partial 			转换为可选
// Readonly 		转换为只读
// Record  			创建值类型相同的复杂对象类型

// Pick  			包含指定属性(用于复杂对象类型)
// Omit  			排除指定属性(用于对象复杂类型)

// Exclude  		取差集(用于联合类型)
// Extract  		取交集(用于联合类型)

索引类型签名

当不知道一个对象的键名时又想给其指定类型,可以使用索引类型签名

映射类型

映射类型可以创建一个拥有另一个联合类型中键名的新类型

根据对象类型创建

索引查询类型

ts 中可以像 js 中获取对象值一样获取某个类型的某个属性的类型

其他用法

可以用索引查询类型创建一个新的联合类型

自定义类型声明文件

.d.ts 文件最终不会生成 js 文件,也不能在里面写入可执行代码,仅作为类型提供

为已存在的 js 项目提供类型声明

具体写法

react 使用

在 react(ts) 中出现在 src 目录下的 .d.ts 类型声明文件无需引入会自动加载

在 tsconfig.json 配置文件中的 include :['src'] 就表示 src 目录下的 .d.ts 类型声明文件会自动加载

内置对象

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

typescript
/* 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

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

一些注意点

typescript
class myclass {
  html: HTMLElement
  constructor() {
    // 这里不加末尾的叹号会报错,因为编译器认为可能获取不到这个 HTML 元素,在末尾加上叹号表示保证会获取到
    this.html = document.getElementById('my')!
  }
}

vue 3

起步

创建项目

bash
## 安装或者升级
npm install -g @vue/cli
vue -V ## 查看脚手架版本,保证 vue cli 版本在 4.5.0 以上
## 创建项目
vue create my-project ## vue create 项目名称

文件梳理

main.ts

typescript
// 引入 createApp 函数,创建对应的应用,产生应用的实例对象
import { createApp } from 'vue'
// 引入 app 组件(所有组件的父组件)
import App from './App.vue'
// 创建app 应用返回对应的实例对象,调用 mount 方法挂载
createApp(App).mount('#app')

App.vue

vue
<template>
  <!-- Vue2 中的html模板中必须要有一对根标签,在vue3中没有这个要求 -->
  <img alt="Vue logo" src="./assets/logo.png" />
  <!-- 使用子级组件 -->
  <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</template>

<script lang="ts">
// 这里声明使用 ts 编写
// defineComponent 函数是定义一个组件,内部可以传入一个配置对象
import { defineComponent } from 'vue'
// 引入子组件
import HelloWorld from './components/HelloWorld.vue'
// 暴露出去一个定义好的组件
export default defineComponent({
  // 组件名称
  name: 'App',
  // 注册组件
  components: {
    HelloWorld
  }
})
</script>

Composition API(常用部分)

setup

  • 新的 option, 所有的组合 API 函数都在此使用, 只在初始化时执行一次
  • 函数如果返回对象, 对象中的属性或方法, 模板中可以直接使用
vue
<template>
  <!-- html 模板中使用 arr 数据时不需要写成 arr.value -->
  <div>{{ arr }}</div>
  <button @click="update">加1</button>
</template>

<script lang="ts">
import { defineComponent, ref, h } from 'vue'
export default defineComponent({
  name: 'App',
  // setup 是组合 API 的入口函数
  setup() {
    // 定义变量(属性)
    //let arr = 0; // 此时的数据不是响应式的
    // 一般用来定义一个基本类型的响应式数据
    let arr = ref(0) // ref 是一个函数,可以定义一个响应式数据,返回的是一个 Ref 对象,对象中有一个 value 属性,如果需要对数据进行操作,需要使用该 Ref 对象调用 value 属性的方式进行数据的操作
    // 定义方法
    function update() {
      // arr++ 这样写是不对的
      arr.value++ // 这样写才能实现
    }
    return {
      // 属性
      arr,
      // 方法
      update
    }
    // 返回一个函数 (渲染函数)
    // return ()=>h('h1','这种写法返回渲染函数 rander')
  }
})
</script>
  • 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 对象中的属性数据
vue
<script lang="ts">
import { defineComponent, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
export default defineComponent({
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return {
      count: 10
    }
  },
  // setup 是组合 API 的入口函数,在 beforeCreate 钩子之前执行且只执行一次,所以在 setup 函数中的 this 不是组件实例
  // 所以不能通过 this 来访问 data/computed/methods / props 以及所有的 composition API 相关回调函数中也都不可以
  setup() {
    let msg = ref('这是一段文字')
    function un() {
      console.log('un执行')
    }
    return {
      msg,
      un
    }
  },
  mounted() {
    // setup 返回的属性和 data 中的属性会合并为组件的属性
    console.log(this.msg)
    console.log(this.count)
    // setup 返回的方法和 methods 中的方法会合并为组件的方法
    this.un()
    this.show()
  },
  methods: {
    show() {
      console.log('show执行')
    }
  }
})
</script>
  • setup 的参数
    • setup(props, context) / setup(props, {attrs, slots, emit})
    • props: 包含 props 配置声明且传入了的所有属性的对象
    • attrs: 包含没有在 props 配置中声明的属性的对象, 相当于 this.$attrs
    • slots: 包含所有传入的插槽内容的对象, 相当于 this.$slots
    • emit: 用来分发自定义事件的函数, 相当于 this.$emit

父组件

vue
<template>
  <h3>父级{{ msg }}</h3>
  <button @click="msg += '??'">更新</button>
  <HelloWorld :msg="msg" :msg2="count" @zidingyi="show" />
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
export default defineComponent({
  name: 'App',
  components: {
    HelloWorld
  },
  data() {
    return {
      count: 10
    }
  },
  setup() {
    let msg = ref('只是一段文字')
    function show(txt: string) {
      msg.value += txt
    }
    return {
      msg,
      show
    }
  }
})
</script>

子组件

vue
<template>
  <h1>子组件:{{ msg }}</h1>
  <button @click="zidingyi">子传父</button>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'HelloWorld',
  props: ['msg'],
  //setup(props, context) { context 参数可以直接解构为 { attrs, emit, slots }
  setup(props, { attrs, emit, slots }) {
    // props 参数是一个对象,里面有父级组件向子级传递的所有属性(也就是 props 里的数据,如果子组件未接收则不会有)
    console.log(props)
    // context 参数是一个对象,里面有 attrs 对象(获取当前组件标签上的属性,但是该属性是在 props 中没有声明接收的所有的数据)、emit方法(分发事件的)、slots对象(插槽)
    console.log(attrs.msg2)
    function zidingyi() {
      emit('zidingyi', '??') // 类似于 vue2 中的 this.$emit()
    }
    return {
      zidingyi
    }
  }
})
</script>

ref

  • 作用: 定义一个数据的响应式
  • 语法: const xxx = ref(initValue):
    • 创建一个包含响应式数据的引用(reference)对象
    • js 中操作数据: xxx.value
    • 模板中操作数据: 不需要.value
  • 一般用来定义一个基本类型的响应式数据
  • ref通常是用于定义基本数据类型,其本质是基于 Object.defineProperty() 重新定义属性的方式实现,vue3 源码中是基于类的属性访问器实现(本质也是 defineProperty )

用 ref 获取页面元素

vue
<template>
  <input type="text" ref="inputRef" />
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
  setup() {
    const inputRef = ref<HTMLElement | null>(null)
    onMounted(() => {
      inputRef.value && inputRef.value.focus() // 当页面加载完毕后自动获取焦点
    })
    return {
      inputRef
    }
  }
})
</script>

reactive

  • 作用: 定义多个数据的响应式
  • const proxy = reactive(obj): 接收一个普通对象然后返回该普通对象的响应式代理器对象
  • 响应式转换是“深层的”:会影响对象内部所有嵌套的属性
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
vue
<template>
  <h3>姓名:{{ user.name }}</h3>
  <h3>年龄:{{ user.age }}</h3>
  <h3>媳妇:{{ user.wife }}</h3>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue'
export default defineComponent({
  name: 'App',
  // setup 是组合 API 的入口函数
  setup() {
    const obj = {
      name: '小名',
      age: 20,
      wife: {
        name: '小红',
        age: 18,
        cars: ['奔驰', '宝马', '奥迪']
      }
    }
    // 返回的是一个 Proxy 的代理对象,被代理者就是传入的对象,无论传入的对象有多复杂多深都是响应式的
    const user = reactive(obj) // 这里也可以直接将数据传入其中
    const update = () => {
      // 直接使用目标对象的方式来更新目标对象中的值不是响应式的,只能使用代理对象的方式来更新数据(响应式数据)
      // obj.name +='@'
      user.name += '?'
      user.age += 2
      user.wife.name += '!'
      user.wife.cars[0] = '玛莎拉蒂' // vue3 中这种写法修改数组也是响应式的
      user.wife.cars[3] = 'xxx' // 直接添加一个新的值也是响应式的
    }
    return {
      user,
      update
    }
  }
})
</script>

响应式丢失

vue
<script>
 setup(){
     const msg = reactive({
         name:'2'
     })
     return{
...msg // reactive 不要使用结构赋值或者展开运算符,这样会导致这个数据丢失响应式
         toRefs(...msg) // 这样才不会丢失响应式
     }
 }
</script>

reactive 与 ref-细节

  • 是 Vue3 的 composition API 中 2 个最重要的响应式 API
  • ref 用来处理基本类型数据, reactive 用来处理对象(递归深度响应式)
  • 如果用 ref 对象/数组, 内部会自动将对象/数组转换为 reactive 的代理对象
  • ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持
  • reactive 内部: 通过使用 Proxy 来实现对对象内部所有数据的劫持, 并通过 Reflect 操作对象内部数据
  • ref 的数据操作: 在 js 中要.value, 在模板中不需要(内部解析模板时会自动添加.value)
vue
<template>
  <h3>m1{{ m1 }}</h3>
  <h3>m2{{ m2 }}</h3>
  <h3>m3{{ m3 }}</h3>
  <button @click="update">更新</button>
</template>

<script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
export default defineComponent({
  setup() {
    const m1 = ref('abc')
    const m2 = reactive({
      name: '小名',
      wift: {
        name: '小花'
      }
    })
    // ref 中如果放入的是一个对象,那么是经过了 reactive 的处理,形成了一个 Proxy 类型的对象
    const m3 = ref({
      name: '小黑',
      wift: {
        name: '小白'
      }
    })
    function update() {
      m1.value += '--'
      m2.wift.name += '--'
      m3.value.name += '--'
      m3.value.wift.name += '--'
    }
    return {
      m1,
      m2,
      m3,
      update
    }
  }
})
</script>

vue2 的响应式

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

Vue3 的响应式

核心:

  • 通过 Proxy(代理): 拦截对 data 任意属性的任意(13 种)操作, 包括属性值的读写, 属性的添加, 属性的删除等...
  • 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 目标对象
      const obj = {
        name: '小名',
        age: 20,
        wife: {
          name: '小红',
          age: 18,
          cars: ['奔驰', '宝马', '奥迪']
        }
      }
      // 把目标对象变成代理对象
      // 参数1 目标对象(obj),参数2 handler 处理器对象,用来监视数据及数据的操作
      const proxyObj = new Proxy(obj, {
        // 拦截代理对象读取操作
        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)
        }
      })
      console.log(proxyObj.name)
      proxyObj.gender = '男'
      console.log(obj)
      delete proxyObj.name
      console.log(obj)
      proxyObj.wife.name = 'xxx' // 属性再深也能拦截到
      console.log(obj)
    </script>
  </body>
</html>

计算属性与监视

  • computed 函数:
    • 与 computed 配置功能一致
    • 只有 getter
    • 有 getter 和 setter
  • watch 函数
    • 与 watch 配置功能一致
    • 监视指定的一个或多个响应式数据, 一旦数据变化, 就自动执行监视回调
    • 默认初始时不执行回调, 但可以通过配置 immediate 为 true, 来指定初始时立即执行第一次
    • 通过配置 deep 为 true, 来指定深度监视
  • watchEffect 函数
    • 不用直接指定要监视的数据, 回调函数中使用的哪些响应式数据就监视哪些响应式数据
    • 默认初始时就会执行第一次, 从而可以收集需要监视的数据
    • 监视数据发生变化时回调
vue
<template>
  <fieldset>
    <legend>姓名操作</legend>
    姓氏:<input type="text" placeholder="输入姓氏" v-model="name.a" /><br />
    名字:<input type="text" placeholder="输入名字" v-model="name.b" /><br />
  </fieldset>
  <fieldset>
    <legend>演示</legend>
    名字:<input type="text" placeholder="显示" v-model="fullname1" /><br />
    名字:<input type="text" placeholder="显示" v-model="fullname2" /><br />
    名字:<input type="text" placeholder="显示" v-model="fullname3" /><br />
  </fieldset>
</template>

<script lang="ts">
import {
  computed,
  defineComponent,
  reactive,
  ref,
  watch,
  watchEffect
} from 'vue'
export default defineComponent({
  setup() {
    const name = reactive({
      a: '东方',
      b: '不败'
    })
    // vue3 中的计算属性需要引入才能使用,如果计算属性中只传入一个回调函数,表示的是 get,返回一个 Ref 对象
    const fullname1 = computed(() => name.a + '_' + name.b)
    const fullname2 = computed({
      get() {
        return name.a + '_' + name.b
      },
      set(val: string) {
        const names = val.split('_')
        name.a = names[0]
        name.b = names[1]
      }
    })
    // vue3 中的侦听器需要引入才能使用,第一个参数表示需要监听的数据,第二个是处理函数其参数就是最新改变的值
    const fullname3 = ref('')
    watch(
      name,
      ({ a, b }) => {
        fullname3.value = a + '_' + b
        // immediate 属性指定侦听器页面加载时默认执行一次,deep 属性表示对目标数据深度监视
      },
      { immediate: true, deep: true }
    )
    // 监视,需要引入才能使用,对比 watch 不需要配置 immediate 本身就会默认执行一次
    // watchEffect(() => {
    //   fullname3.value = name.a + "_" + name.b;
    // });
    // 实现监视 fullname3 的数据,改变 name里的 a 和 b 属性
    watchEffect(() => {
      const names = fullname3.value.split('_')
      name.a = names[0]
      name.b = names[1]
    })
    // watch 可以监听多个数据,
    // watch([name.a,  name.b], () => {
    //当监听的数据不是响应式的时候需要这样写
    watch([() => name.a, () => name.b], () => {
      console.log('============')
    })
    return {
      name,
      fullname1,
      fullname2,
      fullname3
    }
  }
})
</script>

目前 watch 的一些问题

监视 reactive 或者 ref 定义的响应式数据时:oldValue(修改前的值)无法正确获取,且强制开启了深度监视(deep 配置失效,ref 定义的数据不会有这种情况)

监视 reactive 定义的响应式数据中的某个属性(这个属性时引用数据类型):deep 配置有效

js
setup() {
    const obj = reactive({
      name: 22,
      age: 99,
      hh: {
        a: {
          b: 2,
        },
      },
    });
    function show() {
      obj.hh.a.b = 3;
    }
    watch(obj, (a, b) => {
      console.log(a, b); // 这种情况下层级再深也能监听到,vue3 中的 watch 默认就开启了深度监视,即使设置 deep:false 也无效
    });
    return {
      obj,
      show,
    };
  },

      setup() {
          const obj = reactive({
              name: 22,
              age: 99,
              hh: {
                  a: {
                      b: 2,
                  },
              },
          });
          function show() {
              obj.hh.a.b = 3;
          }
          watch(
              () => obj.hh,
              (a, b) => {
                  console.log(a, b);// 这种情况下,不会触发 watch,但如果配置 { deep: true } 则可以监视到
              },
          );
          return {
              obj,
              show,
          };
      },

生命周期

与 2.x 版本生命周期相对应的组合式 API

  • beforeCreate -> 使用 setup()
  • created -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

新增的钩子函数

组合式 API 还提供了以下调试钩子函数:

  • onRenderTracked :只要页面更新就会执行,有一个参数传递回调函数,回调函数可以接收一个 event 参数,可以跟踪组件所有的值
  • onRenderTriggered:和 onRenderTracked 类似只是它不会跟踪组件所有的值,而是跟踪发生变化的值
vue
<template>
  <div>子组件</div>
</template>

<script lang="ts">
// vue3里的所有生命周期钩子都比 vue2 对应的钩子执行时机要早且使用时需要引入,需要放在 setup 函数中,但 vue3 兼容 vue2 ,也就是说在 vue3 中可以使用 vue2 中的生命周期钩子
import { defineComponent, onUpdated } from 'vue'

export default defineComponent({
  // 在 vue3 中 beforeDestroy、deactivated 钩子已经变更为 onbeforeUnmount、onunmounted
  setup() {
    onUpdated(() => {})
    return {}
  }
})
</script>

自定义 hook 函数

  • 使用 Vue3 的组合 API 封装的可复用的功能函数
  • 自定义 hook 的作用类似于 vue2 中的 mixin 技术
  • 自定义 Hook 的优势: 很清楚复用功能代码的来源, 更清楚易懂
  • 需求 1: 收集用户鼠标点击的页面坐标
vue
<template>
  <h3>x:{{ x }}</h3>
  <h3>y:{{ y }}</h3>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import mouse from './mixi/hunru'
export default defineComponent({
  setup() {
    let { x, y } = mouse()
    return {
      x,
      y
    }
  }
})
</script>

hunru.ts

typescript
import { onBeforeMount, onMounted, ref } from 'vue'
export default function () {
  const x = ref(-1)
  const y = ref(-1)
  const clickhandel = (e: MouseEvent) => {
    x.value = e.pageX
    y.value = e.pageY
  }
  // 在页面渲染完毕之后获取鼠标点击位置
  onMounted(() => {
    window.addEventListener('click', clickhandel)
  })
  // 在页面销毁之前解绑点击事件
  onBeforeMount(() => {
    window.removeEventListener('click', clickhandel)
  })
  return {
    x,
    y
  }
}

toRefs

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

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

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

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

vue
<template>
  <h3>{{ name }}</h3>
  <h3>{{ age }}</h3>
</template>

<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent({
  setup() {
    const state = reactive({
      name: '小花',
      age: 14
    })
    // toRefs 可以把一个响应式对象转换为普通对象,该对象的每个 property 都是一个 ref
    const state2 = toRefs(state)
    // const {name,age} = toRefs(state);也可以直接解构
    // 设置定时器更新数据,如果数据变化了,界面也会随之变化,肯定时响应式数据
    setInterval(() => {
      // state.name += "=="; // 不设置 toRefs
      state2.name.value += '=='
      // name.value += "=="; 如果使用 toRefs 解构的话可以直接修改 name 的值
    }, 1000)
    return {
      // state,
      // name,age 如果使用 toRefs 解构的话就返回解构出来的数据
      ...state2 // 不设置 toRefs这种方式数据不是响应式的
    }
  }
})
</script>

Composition API(其它部分)

shallowReactive 与 shallowRef

  • shallowReactive : 只处理了对象内最外层属性的响应式(也就是浅响应式)
  • shallowRef: 只处理了 value 的响应式, 不进行对象的 reactive 处理
  • 什么时候用浅响应式呢?
    • 一般情况下使用 ref 和 reactive 即可
    • 如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
    • 如果有一个对象数据, 后面会产生新的对象来替换 ===> shallowRef
vue
<template>
  <h1>m1:{{ m1 }}</h1>
  <h1>m2:{{ m2 }}</h1>
  <h1>m3:{{ m3 }}</h1>
  <h1>m4:{{ m4 }}</h1>
  <button @click="show">更新</button>
</template>

<script lang="ts">
import {
  defineComponent,
  reactive,
  ref,
  shallowReactive,
  shallowRef
} from 'vue'
export default defineComponent({
  setup() {
    const m1 = reactive({
      name: '小名',
      age: 20,
      car: {
        name: '奔驰',
        color: 'green'
      }
    })
    const m2 = shallowReactive({
      name: '小名',
      age: 20,
      car: {
        name: '奔驰',
        color: 'green'
      }
    })
    const m3 = ref({
      name: '小名',
      age: 20,
      car: {
        name: '奔驰',
        color: 'green'
      }
    })
    const m4 = shallowRef({
      name: '小名',
      age: 20,
      car: {
        name: '奔驰',
        color: 'green'
      }
    })

    function show() {
      m2.car.name += '!!'
      m1.car.name += '!!'
      m3.value.car.name += '!!'
      m4.value.car.name += '!!'
    }
    return {
      m1,
      m2,
      m3,
      m4,
      show
    }
  }
})
</script>

readonly 与 shallowReadonly

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

<script lang="ts">
import { defineComponent, reactive, readonly, shallowReadonly } from 'vue'
export default defineComponent({
  setup() {
    const m1 = reactive({
      name: '小名',
      age: 20,
      car: {
        name: '奔驰',
        color: 'green'
      }
    })
    // const m2 = readonly(m1);// 此时 m2 为只读属性不可更改且无论层级深度都不可更改
    const m2 = shallowReadonly(m1) // 此时 m2 为只读属性,但不会深度可读
    function show() {
      // m2.name += "!!"; 设置 shallowReadonly 为只读属性,此时不可更改
      m2.car.name += '!!' // 此时 m2 为只读属性,但不会深度可读
      //m2.car.name += "!!"; // 此时 m2 为只读属性不可更改且无论层级深度都不可更改
    }
    return {
      m2,
      show
    }
  }
})
</script>

toRaw 与 markRaw

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

<script lang="ts">
import { defineComponent, markRaw, reactive, toRaw } from 'vue'
export default defineComponent({
  setup() {
    const m1 = reactive<any>({
      name: '小名',
      age: 20
    })
    function show() {
      //toRaw 把代理对象变成普通对象(非响应式数据)
      const user = toRaw(m1)
      user.name += '==='
    }
    function show2() {
      // 后添加的新属性和直接通过索引修改数据都是响应式的
      // m1.car = [""];
      // m1.car[0] = "kk";
      const likes = ['吃', '喝']
      // markRaw 标记的对象数据从此以后都不再能成为代理对象了(非响应式)
      m1.likes = markRaw(likes)
      setInterval(() => {
        m1.likes[0] += '=='
        console.log('执行')
      }, 1000)
    }
    return {
      m1,
      show,
      show2
    }
  }
})
</script>

toRef

  • 为源响应式对象上的某个属性创建一个 ref 对象, 二者内部操作的是同一个数据值, 更新时二者是同步的
  • 区别 ref: 拷贝了一份新的数据值单独操作, 更新时相互不影响
  • 应用: 当要将 某个 prop 的 ref 传递给复合函数时,toRef 很有用
vue
<template>
  <h1>m1:{{ m1 }}</h1>
  <h1>m1:{{ age }}</h1>
  <h1>m1:{{ name }}</h1>
  <button @click="show">更新</button>
  <hr />
  <!-- 此时传递的 age 实际上是 age.value,所以说传递的是 toRef 里的数据  -->
  <child :age="age" />
</template>

<script lang="ts">
import child from './components/HelloWorld.vue'
import { defineComponent, toRef, reactive, ref } from 'vue'
export default defineComponent({
  components: {
    child
  },
  setup() {
    const m1 = reactive({
      name: '小名',
      age: 2
    })
    // 把响应式数据 m1 对象中的某个属性 age 变成了 ref 对象
    const age = toRef(m1, 'age')
    // 把响应式对象中的某个属性使用 ref 进行包装,变成一个 ref 对象
    const name = ref(m1.name)
    function show() {
      m1.age += 2
      age.value += 3
      name.value += 'rr' // 此时修改的是用 ref 包装后的数据与源数据 m1里的 name 无关,所以这里的操作不会影响 m1
    }
    return {
      m1,
      name,
      age,
      show
    }
  }
})
</script>

HelloWorld.vue

vue
<template>
  <div>age:{{ age }}</div>
  <div>length:{{ length }}</div>
</template>

<script lang="ts">
import { computed, defineComponent, toRef, Ref } from 'vue'
// 此处表示该函数的参数需要一个 Ref 对象
function getLength(age: Ref) {
  return computed(() => {
    return age.value.toString().length
  })
}
export default defineComponent({
  props: {
    age: {
      type: Number
    }
  },
  setup(props) {
    // 此处调用 getLength 时必须传递一个 ref 对象,所以不能直接传递 props.age,必须通过 toRef 包装
    const length = getLength(toRef(props, 'age'))
    return { length }
  }
})
</script>

customRef

  • 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
  • 需求: 使用 customRef 实现 防抖示例
vue
<template>
  <h1>m1:{{ key }}</h1>
  <input type="text" v-model="key" />
</template>

<script lang="ts">
// import child from "./components/HelloWorld.vue";
import { customRef, defineComponent, ref } from 'vue'
// 自定义防抖函数,将来的数据类型不确定所以用泛型
function useDebouncedRef<T>(value: T, delay = 500) {
  let timeoutId: number
  return customRef((track, trigger) => {
    return {
      // 获取数据
      get() {
        // 告诉 Vue 追踪数据
        track()
        return value
      },
      set(newValue: T) {
        clearTimeout(timeoutId)
        timeoutId = setTimeout(() => {
          value = newValue
          // 告诉 Vue 更新页面
          trigger()
        }, delay)
      }
    }
  })
}
export default defineComponent({
  setup() {
    // const key = ref("abc");
    const key = useDebouncedRef('abc', 500)
    return {
      key
    }
  }
})
</script>

provide 与 inject

  • provideinject提供依赖注入,功能类似 2.x 的provide/inject
  • 实现跨层级组件(祖孙)间通信
vue
<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>
vue
<template>
  <div>
    <h2>子组件</h2>
    <hr />
    <GrandSon />
  </div>
</template>

<script lang="ts">
import GrandSon from './GrandSon.vue'
export default {
  components: {
    GrandSon
  }
}
</script>
vue
<template>
  <h3 :style="{ color }">孙子组件: {{ color }}</h3>
</template>

<script lang="ts">
import { inject } from 'vue'
export default {
  setup() {
    const color = inject('color')

    return {
      color
    }
  }
}
</script>

响应式数据的判断

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

手写组合 API

shallowReactive 与 reactive

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      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) {
        // 判断当前传入的是否是复杂数据类型
        if (obj && typeof obj === 'object') {
          return new Proxy(obj, reactiveHandler)
        }
        return obj
      }

      /*
        自定义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
    </script>
  </body>
</html>

shallowReadonly 与 readonly

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      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) {
        // 判断当前传入的是否是复杂数据类型
        if (obj && typeof obj === 'object') {
          return new Proxy(obj, readonlyHandler)
        }
        return obj
      }

      /*
自定义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
    </script>
  </body>
</html>

shallowRef 与 ref

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      /*
自定义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)
    </script>
  </body>
</html>

isRef, isReactive 与 isReadonly

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      /*
判断是否是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({})))
    </script>
  </body>
</html>

新组件

Fragment(片断)

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

Teleport(瞬移)

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

<script lang="ts">
import child from './components/HelloWorld.vue'
import { defineComponent } from 'vue'
export default defineComponent({
  components: {
    child
  }
})
</script>

HelloWorld.vue

vue
<template>
  <button @click="show = !show">打开</button>

  <teleport to="body">
    <!-- 此时在 teleport 包裹的元素在渲染后会传送到 body 标签下而不是 app 下,to 后面填写选择器-->
    <div v-if="show">这是对话框</div>
  </teleport>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
  setup() {
    const show = ref(false)
    return { show }
  }
})
</script>

Suspense 及路由懒加载

  • Suspense 允许我们的应用程序在等待异步组件时渲染一些后备内容,可以让我们创建一个平滑的用户体验
vue
<template>
  <h1>父级</h1>
  <hr />
  <Suspense>
    <template #default>
      <!-- 异步组件 #default 固定写法-->
      <child />
    </template>
    <template v-slot:fallback>
      <!-- 异步组件之前的 loding 组件 v-slot:fallback 固定写法 -->
      <h3>loading</h3>
    </template>
  </Suspense>
</template>

<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'
// import child from "./components/HelloWorld.vue";
// vue2 中动态引入组件的方法,在 vue3 中不可用
// const child = () => import("./components/HelloWorld.vue");
// 在 vue3 中引入动态组件应该这样写
const child = defineAsyncComponent(() => import('./components/HelloWorld.vue'))
import child from './components/HelloWorld.vue'
export default defineComponent({
  components: {
    child
  }
})
</script>

HelloWorld.vue

vue
<template>
  <h2>子</h2>
  <h2>{{ arr }}</h2>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  setup() {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({
          arr: 'haol'
        })
      }, 2000)
    })
  }
})
</script>

这样页面会先显示 loading 过 2s 才显示组件内容

值得注意的是路由懒加载和异步组件不同,vue2 中怎么写 vue3 中还怎么写

js
// vue2 中动态引入组件的方法,在 vue3 中不可用
// const child = () => import("./components/HelloWorld.vue");
// 在 vue3 中引入动态组件应该这样写
const child = defineAsyncComponent(() => import('./components/HelloWorld.vue'))
// 但值得注意的是路由懒加载和异步组件不同,vue2中怎么写vue3中也怎么写

vue3 的一些变化

新增 emits

js
export default {
  // 在 vue3 中新增 emits 用于声明组件派发的所有事件,并且强烈建议所有通过 emit 派发的事件都在此声明
  // 因为 vue3 移除了 .native 修饰符,任何没有在 emits 中声明的事件将会自动添加到 $attrs 中,而 attrs 默认情况下是绑定到根标签的
  porps: ['text'],
  emits: ['myclick']
}
vue
<template>
  <div>
    <button @click="$emit('click')">点击</button>
  </div>
</template>

<script>
export default {
  // 这种情况下实际上 click 是绑定在根标签 div 上的
}
</script>

添加全局属性

js
// 在vue2中我们通常在 Vue 的原型对象上添加全局可用的属性或方法
Vue.prototype.$http = () => {}
// 在 vue3 中使用如下写法定义
const app = createApp({})
app.config.globalProperties.$http = () => {}
app.config.globalProperties.$me = { name: '信息' }
vue
<template>
  <!-- 在模板中使用 -->
  <span @click="$http()">{{ $me.name }}</span>
</template>

<script>
import { getCurrentInstance } from 'vue'
export default {
  setup() {
    // 在js中使用
    const { proxy } = getCurrentInstance()
    console.log(proxy.$me)
  }
}
</script>

ts 中使用

在入口文件中添加全局属性

typescript
const app = createApp(App)
app.config.globalProperties.$myFun = (val: string | number) => {
  console.log(val)
}
app.config.globalProperties.$mydata = '555'

在 src 新建一个 test.d.ts 扩展类型

typescript
// 如果项目中有 xxx.d.ts 的类型声明文件直接写入如下代码,如没有则新建
// 注意扩展 vue 的接口需要先引入 vue
import Vue from 'vue'
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $myFun: Function
    $mydata: string
  }
}

在 tsconfig.json 中配置一下,把新建的 test.d.ts 路径添加进去

json
"include":[
    "src/test.d.ts"
]

在组件中使用

vue
<template>
  <span @click="$myFun(1)">{{ $mydata }}</span>
</template>

<script>
import { getCurrentInstance,ComponentInternalInstance } from 'vue'
export default {
	setup(){
        // 由于 getCurrentInstance 返回值类型为 ComponentInternalInstance | null
        // 所以需要先用类型断言成 ComponentInternalInstance,这样就有语法提示了
		const {proxy} = getCurrentInstance() as ComponentInternalInstance
        proxy?.$myFun(1)
        console.log(proxy?.$mydata)
    }
}
</script>

script setup 编译时语法糖使用

test2.vue 子组件

vue
<template>
  {{ title }}
  <button @click="hand">修改</button>
  <!-- 在模板中可以通过$emit、$props 直接访问 emit 和 props  attrs和slots也是如此-->
</template>
<script lang="ts" setup>
import { useSlots, useAttrs } from '.vue'
// 接收参数
const props = defineProps(['title'])
// 自定义事件
const emit = defineEmits(['changeTitle'])
// 使用 slots
const slots = useSlots()
// 使用 attrs
const attrs = useAttrs()
function hand() {
  emit('changeTitle', '修改后')
}
// 使用语法糖必须使用 defineExpose 向外暴露属性
defineExpose({
  hand
})
</script>

父组件

vue
<template>
  <Test2 :title="title" @changeTitle="changeTitle" ref="test" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from '.vue'
// 使用语法糖时引入组件时会自动注册
import Test2 from './test2.vue'
// 声明的属性或方法无需 return 即可在模板中使用
const title = ref('test2')
const test = ref(null)
function changeTitle(val: string) {
  title.value = val
}
onMounted(() => {
  // 使用语法糖通过 ref 获取组件时,只能获取到该组件 defineExpose 暴露的属性
  console.log(test.value.hand)
})
</script>

VNode hooks

在每个组件或 HTML 标签上可以使用一些特殊的钩子作为事件监听器这些钩子有

js
// onVnodeBeforeMount
// onVnodeMounted
// onVnodeBeforeUpdate
// onVnodeUpdated
// onVnodeBeforeUnmount
// onVnodeUnmounted

// 需要注意的是,这些钩子向回调函数传递一些参数。除了 onVnodeBeforeUpdate 和 onVnodeUpdated 传递两个参数,即当前的 VNode 和之前的 VNode,其它只传递一个参数,即当前的 VNode。
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)

function onMyComponentMounted() {}

function divThatDisplaysCountWasUpdated() {}
</script>

<template>
  <MyComponent @vnodeMounted="onMyComponentMounted" />
  <div @vnodeUpdated="divThatDisplaysCountWasUpdated">{{ count }}</div>
</template>

防止内存泄漏

effectScope 是 Vue3 引入的一个副作用作用域工具,用于将多个响应式副作用(effect、watch、watchEffect、computed 等)包裹在一个独立的作用域中,实现批量管理。当作用域被销毁时,内部所有副作用会被自动清理,避免内存泄漏并简化代码逻辑。

ts
// useMouse.ts 公共 Hook
import { effectScope, reactive, watch, onScopeDispose } from "vue"

export function useMouse() {
  const scope = effectScope()
  const pos = reactive({ x: 0, y: 0 })

  scope.run(() => {
    const update = (e: MouseEvent) => {
      pos.x = e.clientX
      pos.y = e.clientY
    }
    watch(pos, () => {
      console.log(pos, '鼠标动了')
    })
    window.addEventListener('mousemove', update)
    // onScopeDispose 是一个注册回调函数的方法,该回调会在所属的 effectScope 被停止 (scope.stop()) 时执行
    onScopeDispose(() => window.removeEventListener('mousemove', update))
  })
  return {
    pos,
    stop: () => scope.stop() // 关键:一键停止所有副作用
  }
}

在组件中使用 Hook

在执行 Hook 导出的 stop 后 Hook 中的所有副作用均被销毁,即 watch、mousemove事件、定时器等全部清除

vue
<script setup lang="ts">
import { useMouse } from '@/util'

const { stop, pos } = useMouse()
const { stop: stop2, pos: pos2 } = useMouse()
</script>

<template>
  <span>副作用1。x:{{ pos.x }},y:{{ pos.y }}</span>
  <button @click="stop">停止1所有副作用</button>
  <div>
    <span>副作用2。x:{{ pos2.x }},y:{{ pos2.y }}</span>
    <button @click="stop2">停止2所有副作用</button>
  </div>
</template>