基于TypeScript的Vue3.0初体验

1,302 阅读18分钟

一、你好,TypeScript

1.1 环境

全局安装
npm install -g typescript
创建项目结构

1. mkdir ts-basic && cd ts-basic

2. touch index.ts

3. npm init -y 

4. tsc --init  //(生成 tsconfig.json)

5. 配置package.json

package.json加入编译命令

{
    "name": "ts-basic",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "tsc index.ts",
        "build:w": "tsc index.ts -w"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
}

index.ts中编写完ts代码之后, 可以在终端npm run build的方式编译, 会生成一个转化后的index.js文件

1.2 原始数据类型

TypeScript的原始类型包括:

  • boolean
  • number
  • string
  • void
  • undefined
  • null
  • symbol
  • bigint
// 布尔类型
let isDone: boolean = false

// 数字类型: 支持二进制、十进制、十六进制等

let age: number = 6
let b: number = 0xf00d
let c: number = 0b1010
let d: number = 0o744

//字符串,支持模版语法
let firstName: string = "chan";
let secondName: string = `hello , ${firstName} , age is ${age}`;


// 空值: 函数无返回数据时,使用void
function warnDialog:void{
    alert('提示')
}

// 特别注意, null,undefined 
let u: undefined = undefined;
let n: null = null;

// 注意 undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:
let nu: number = undefined;


//any类型, 可以匹配任意类型,但不推荐使用
let noSure: any = 4;
noSure = "maybe is string";
noSure = false;

//在任意值上访问不存在的属性,都是允许的,不会编译出错
noSure.name = "chonglou";

//也允许调用方法,不会编译出错
noSure.getName();

1.3 数组和元组

数组

数组有两种类型定义方式,一种是使用泛型:

const list: Array<number> = [1, 2, 3]

一种是比较简写的类型声明:

const list: number[] = [1, 2, 3]
元组

基本使用和数组类似, 表示一个已知元素数量和类型的数组,各元素类型可不相同

const list:[string,number] = ['123',12]

注意:


1. 类型、数量、顺序需要和声明时一致,否则会出错

list = ['123',12, true]  //ERROR
 
list = [123,'12']  //ERROR

2. 允许添加元素, 但必须和声明时的类型相同, 但访问的时候,会出现数组越界

list.push('string') //OK
list.push(224) //OK
list.push(true) //ERROR

console.log(list[2]) //ERROR

1.4 接口(interface)

 作用: 具备“Duck Typing” , 关注其属性类型本身的变化, 且没有实现, 为项目代码或者第三方代码定义契约
基本定义
interface Person {
  name: string;
  age: number;
}

let man: Person = {
    name: "chonglou",
    age:10
};


//同时可以使用propName来容纳非声明的任意元素
interface Person {
  name: string;
  age: number;
  [propName: string]:any;
}

//成立
const xiaoming : Person = {
     name: "chonglou",
     age:10,
     city:'beijing'
};
可选属性

使用 “?” 来表明属性可选

interface Person {
  name: string;
  age?: number;
}

let man: Person = {
    name: "chonglou",
};
只读属性

使用 “readonly” 修饰 来表明属性只读

interface Person {
  readonly id: number;
  name: string;
  age?: number;
}

let man: Person = {
  id: 20,
  name: "chonglou",
  age: 18,
};

console.log(man.id);

//不可修改
// man.id = 30;

1.5 函数

定义函数参数类型

定义传入参数类型, 约定输入和输出类型

function add(x: number, y: number): number {
  return x + y;
}
可选参数

在参数后面加上“ ? ” 即代表参数可能不存在,但可选参数后面,不可以追加确定参数, 只能是可选参数

function add(x: number, y: number, z?: number): number {
  if (typeof z === "number") {
    return x + y + z;
  }
  return x + y;
}


//不允许(t参数需为可选)
function add1(x: number, y: number, z?: number, t: number): number {
   ...
}

//可行
function add1(x: number, y: number, z?: number, t?: number): number {
   ...
}
默认参数
const reduce = (a: number, b = 10) => a - b;
剩余参数

和JavaScript类似, 使用...rest剩余拓展符来获取剩余参数, 返回一个数组

const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a)
重载

以传入参数和方法签名 在方法签名列表中从上往下查找逐个匹配,直到匹配到完全匹配的选项. 一般签名比较具体的,建议放在最上面

在日常开发第三方库的时候, 都会常用函数重载来限制暴露出去的方法的场景类型

interface Point{
    x:number;
    y?:number;
    z?:number;
}

function assigned(all: number): Point;
function assigned(x: number, y: number,z:number): Point;

function assigned(x: number, y?: number, z?: number): Point {
  if (y === undefined && z === undefined) {
    y = z = x;
  } else if (z == undefined) {
    z = 0;
  }
  return {
    x: x,
    y: y,
    z: z,
  };
}

//OK
assigned(1);

//ERROR: 方法签名列表匹配不到该方式
assigned(10, 20);

//OK
assigned(10, 20, 30)

1.6 常见类型场景使用

类型推论

TypeScript 自带的类型推导会对生成的数据或函数定义一个结果类型, 同时,解构的时候,也具备同样的作用

//1. 函数返回值类型推导

function sayHi(name: string) {
    return "Hello, " + name
}
//sayHi函数自动加上了返回值类型:  string


//2. 多类型联合推导

let arr = [1,'string',false]
//编译器会为 arr 推导出一个联合类型: number | string | boolean

//3. 解构推导

const list = [1, 2];
let [a, b] = list;
a = 'string' // Error:不能把 'string' 类型赋值给 'number' 类型
联合类型

使用“ | ” 作为标记 , 并集多个类型

let numberOrString: number | string 

//当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,只能访问此联合类型的所有类型里共有的属性或方法

let a = numberOrString.length  //ERROR
let str = numberOrString.toString()  //ERROR
交叉类型

使用“ & ” 作为标记 , 并集多个类型

interface IName  {
  name: string
}
type IPerson = IName & { age: number }
let person: IPerson = { name: 'hello', age: 12}
类型别名

类型别名,就是给类型起一个别名,让它可以更方便的被重用。


//支持联合类型
let StringOrNumber = string | number;
let result: StringOrNumber = '123'
result = 123


//支持字面量
type Directions = 'Up' | 'Down' | 'Left' | 'Right'
let toWhere: Directions = 'Up'

//支持泛型
type Container<T> = { value: T };
let cs: Container<number> = { value: 1 };

...

类型断言

使用“ as ” 作为标记 , 告诉typescript 编译器, 既然你没有办法判断此时的数据类型, 那么我直接告诉你, 当前数据时哪种类型

function getLength(input: string | number): number {
  const str = input as string;
  if (str.length) {
    return str.length;
  } else {
    const number = input as number;
    return number.toString().length;
  }
}

需要注意的是, 类型断言不可以滥用, 除非能确保程序是严谨安全的情况下使用, 否则就丧失了TypeScript 本身指定类型要求的作用了. 同时在某些场景中, 也允许双重断言:

interface Person{
    name:string;
    age:number;
}

const person = 'chonglou' as Person; //Error

const person1 = 'chonglou' as any as Person; // OK

类型守卫

缩小类型的检测范围来间接达到断言效果的作用

typeof

function getLength(input: string | number): number {
  if (typeof input === "string") {
    return input.length;  // 已指定是string类型了
  } else {
    return input.toString().length;
  }
}

instanceof

class Person{
    name:'chonglou';
    age:20;
}


class Animal{
    name:'xiaobao';
    color:'red';
}

function getSometing(arg: Person | Animal){

    //arg类型已细化为 Person
    if (arg instanceof Person) {
        console.log(arg.color); // Error
    }
    
     //arg类型已细化为 Animal
    if (arg instanceof Animal) {
        console.log(arg.age); // Error
    }
}

in

x in y 表示 x 属性在 y 中存在

function getSometing(arg: Person | Animal){
    //arg类型已细化为 Person
    if ('age' in Person) {
        console.log(arg.color); // Error
    }
    
     //arg类型已细化为 Animal
    if ('color' in Animal) {
        console.log(arg.age); // Error
    }
}

字面量类型

字面量类型可以表示唯一时,则细化类型


type add = {
    type:'add', // 字面量类型
    count:number
}

type reduce = {
    type:'reduce', // 字面量类型
    total:number
}


function getAction(action: add | reduce){

    //类型细化为 add
    if(action.type === 'add'){
         console.log(action.total); // Error
    }
    
    //类型细化为 reduce
    if(action.type === 'reduce'){
         console.log(action.count); // Error
    }
}

1.7 Class

访问修饰符

1. public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public2. private 修饰的属性或方法是私有的, 只能在本类内部使用

3. protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  run() {
    return `${this.name} is running`;
  }
}

const snake = new Animal("mirror");
console.log(snake.run());

//继承的特性
class Dog extends Animal {
  bark() {
    return `${this.name} is barking`;
  }
}

const xiaohuang = new Dog("xiaohuang");
console.log(xiaohuang.run());
console.log(xiaohuang.bark());

类具备继承性, 在其子类构造函数中,必须使用 super 调用父类方法, 否则会报错

class Cat extends Animal {
  constructor(name) {
    super(name)
    console.log(this.name)
  }
  run() {
    return 'Meow, ' + super.run()
  }
}
const maomao = new Cat('maomao')
console.log(maomao.run())
类和接口的关系

接口定制类

一个类只能继承另一个类,但可以实现多个接口, 使用implements关键字定制
interface Radio {
  switchRadio(trigger: boolean): void;
}

interface Battery {
  checkBatteryStatus(): void;
}

class Car implements Radio {
  switchRadio(trigger: boolean): void {
    console.log(`汽车${trigger ? "打开" : "关闭"}音响`);
  }
}

class CellPhone implements Radio, Battery {
  switchRadio(trigger: boolean): void {
    console.log(`手机${trigger ? "打开" : "关闭"}铃声`);
  }
  checkBatteryStatus() {
    console.log("手机快没电了");
  }
}

接口继承类

1. 当一个接口继承了一个类时, 它会继承类的成员但不包括实现
2. 一个接口可以继承于多个接口, 组合成一个超接口
class Point {
  x?: number;
  y?: number;
}

interface Point3d extends Point {
  z: number;
}

let cityAddres: Point3d = { x: 1, y: 2, z: 3 };

1.8 枚举

数字枚举

一个数字枚举可以用 enum 这个关键词来定义,我们定义一系列的方向,然后这里面的值,枚举成员会被赋值为从 0 开始递增的数字

enum Direction {
    Up,
    Down,
    Left,
    Right
}

console.log(Direction.Up === 0); // true
console.log(Direction.Down === 1); // true
console.log(Direction.Left === 2); // true
console.log(Direction.Right === 3); // true

//而且允许反向映射
console.log(Direction[0])
字符串枚举
enum Direction {
    Up = 'Up',
    Down = 'Down',
    Left = 'Left',
    Right = 'Right'
}

console.log(Direction['Right'], Direction.Up); // Right Up
异构枚举
enum StringAndNumberEnum {
    top = 'top',
    bottom = 2
}

1.9 泛型

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

在日常开发过程中, 我们声明一个函数,会根据参数类型的传入, 来约束返回值的输出, 但往往会限制在一个类型中使用, 而不能多个类型使用:

function returnItem (arg: number): number {
    return arg
}


const result = returnItem(123) //OK
const result1 = returnItem('2123') //Error

根据上述函数描述的场景, 只能传入number 类型的数据, 而无法根据使用场景, 动态的传入所需要的具体类型, 在此, 我们可以引入泛型的使用:

function returnItem<T>(arg: T): T {
    return arg
}

const r1 = returnItem("123");//OK
const r2 = returnItem(1111); //OK
多个类型参数指定

使用元组来指定多个类型参数的传入并输出

function returnItem<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}
const r3 = returnItem(["string", 123]);
泛型变量

当我们使用一个泛型作为一个参数传入时, 假设其具体类型为数组, 但其内部元素又不确定时, 为了不影响其基本的使用, 又能保证其内部元素可以根据不同使用场景使用时, 可以做到以下修改

//使用Array<T> 来声明,即不影响外层数组本身的属性和方法使用
function getArrayLength<T>(arg: Array<T>) {
  console.log((arg as Array<any>).length); // ok
  return arg;
}
泛型接口
interface KeyPair<T, U> {
  key: T;
  value: U;
}

let kp1: KeyPair<number, string> = { key: 1, value: "str" };
let kp2: KeyPair<string, number> = { key: "str", value: 123 };


//函数方式
interface ReturnItemFn<T> {
  (para: T): T;
}

const returnItem: ReturnItemFn<number> = (para) => para;
const r4 = returnItem(1);
泛型类
//根据不同场景,指定其元素的类型
class Queue<T> {
  private arr: T[] = [];
  push(item: T) {
    this.arr.push(item);
  }
  pop() {
    this.arr.pop();
  }
}

const queue = new Queue<string>();
queue.push('1111')

泛型约束

可以通过类型别名来指定类型,来限制泛型的输入

type params = string | number;

class Queue<T extends params> {
  private arr: T[] = [];
  push(item: T) {
    this.arr.push(item);
  }
  pop() {
    this.arr.pop();
  }
}

const queue = new Queue<boolean>(); //Error

也可以使用交叉类型的方式进行限制

interface Person{
    getName():string
}


interface Student{
    getNumber():number
}


class XiaoMing <T extends Person & Student> {
     private genericProperty: T
    sayHi(){
        this.genericProperty.getName()
        this.genericProperty.getNumber()
    }
}

...

//或者使用超接口的方式

interface Man extends Person, Student {}
class XiaoMing <T extends Man> {
     private genericProperty: T
    sayHi(){
        this.genericProperty.getName()
        this.genericProperty.getNumber()
    }
}

索引类型

在日常开发过程中, 我们可能会遇到一个这样的场景, 比如:使用关键字key来获取对象上的数据, 又或是通过index来获取数组所对应的下标元素等, 对于这种情况, 我们只是单一的指定泛型类型, 显然是不够通用的:

function getValue<T extends object>(obj: T, key: string) {
   return obj[key]
}


function getArrayChildForIndex<T extends object>(obj: T, index: number) {
   return obj[key]
}

以上方式, 无法确定key 是否存在于obj,或者index在arr范围内, 故而我们可以引入“索引类型”的<U extends keyof T>方式来关联两个泛型的使用

function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
  return obj[key] // ok
}

二、Vue3.0新特性

2.1 全局API

  • 挂载 App 实例

提供 createApp 初始化实例对象并实现挂载

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

同时,多个应用实例之前可共享配置

import { createApp } from 'vue'

import App from './App.vue'
import Other from './Other.vue'

const createMyApp = options => {
  const app = createApp(options)
  app.directive('focus' /* ... */)

  return app
}

createMyApp(App).mount('#app')
createMyApp(Other).mount('#other')
  • 全局配置
config.productionTip 移除
config.ignoredElements 替换为 config.isCustomElement
  • 创建指令的变更
1. Vue.component -> app.component

2. Vue.directive -> app.directiv

3. Vue.mixin -> app.mixin

4. Vue.use -> app.use
  • 提供/注入 (Provide / Inject)

和2.0的provide类似, 3.0还可以提供由应用程序内的任何组件注入的依赖项:

// 在入口
app.provide({
  guide: 'Vue 3 Guide'
})

// 在子组件
export default {
  inject: {
    book: {
      from: guide
    }
  },
  template: `<div>{{ book }}</div>`
}

模版指令

template标签的v-for使用

在vue2.x中 <template> 标签不能拥有key. 只能在其子节点上添加:

<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>

在 Vue 3.x 中 key 则应该被设置在 <template> 标签上

<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>
v-if 与 v-for 的优先级

vue 2.x 版本中在一个元素上同时使用v-ifv-for 时,v-for 会优先作用.

在vue3.0中, 则是 v-if 的优先级会高于 v-for

v-model
  • 自定义model参数和事件名称变更,且组件的model选项已废除, 同v-bind的.sync修饰符也已废除
1. prop: value -> modelValue

2. event: input -> update:modelValue

vue2.x中, 在组件上使用v-model相当于绑定 value prop和事件 input 语法如下:

export default {
   name:'CustomComponent',
   model:{
       prop:'value',
       event:'input'
   },
   props:{
       value:{
          type:String,
          default:'default title'
       }
   }
   methods:{
       onChangeValue(){
           this.$emit('input',this.value);
       }
   }
}

//父组件中
<custom-component v-model='parentValue' />

<!--如: -->
<custom-component :value="parentValue" @input="parentValue = $event" />

同时在某些特定的场景,由于v-model只允许绑定一个prop 为了允许多个数据同步更新的情况下, 采用v-bind.sync来保证多个 prop 间接实现双重数据绑定的效果:

export default {
   name:'CustomComponent',
   model:{
       prop:'value',
       event:'input'
   },
   props:{
       title:String,
       value:{
          type:String,
          default:'default title'
       }
   }
   methods:{
       onChangeValue(){
           this.$emit('input',this.value);
       },
       
       onChangeTitle(){
           this.$emit('update:title',this.title);
       }
   }
}

//父组件中
<custom-component v-model='parentValue' :title.sync="parentTitle" />

<!--如: -->
<custom-component :value="parentValue" @input="parentValue = $event"  :title="parentTitle" @update:title="parentTitle = $event" />

而在vue3.0中, 则是将两者做了一个结合, 同时允许对外使用多个model:


export default {
   name:'CustomComponent',
   props:{
       title:String,
       value:{
          type:String,
          default:'default title'
       }
   }
   methods:{
       onChangeValue(){
           this.$emit('update:value',this.value);
       },
       
       onChangeTitle(){
           this.$emit('update:title',this.title);
       }
   }
}


//父组件中
<custom-component v-model:value='parentValue' v-model:title="parentTitle" />

<!--如: -->
<custom-component :value="parentValue" @update:value="parentValue = $event"  :title="parentTitle" @update:title="parentTitle = $event" />

  • 自定义修饰符

除了.trim.number.lazy等2.0硬性的v-model修饰符外, 在vue3.0允许使用modelModifiers来自定义修饰符:

modelModifiers 可以获取添加到props上的修饰符号

//自定义修饰符capitalize,将传入的数据首字母大写

<ModelModifiers v-model.capitalize ="inputValue" />


export default {
   name:'ModelModifiers',
   props:{
     modelValue: String,
     modelModifiers: {
       default: () => ({ capitalize: false })
     }
   },
   created() {
     console.log(this.modelModifiers) // { capitalize: true }
   },
   methods:{
       onChangeValue(event){
           let value = event.target.value;
           if(this.modelModifiers.capitalize){
               value = value.charAt(0).toUpperCase() + value.slice(1);
           }
           this.$emit('update:modelValue', value)
       },
   }
}

如果是带有参数的v-model修饰符绑定, 生成的prop则是arg + Modifiers:

<ModelModifiers v-model:title.capitalize ="inputValue" />
export default {
   name:'ModelModifiers',
   props:{
     title: String,
     titleModifiers: {
       default: () => ({ capitalize: false })
     }
   },
   created() {
     console.log(this.titleModifiers) // { capitalize: true }
   },
   methods:{
       onChangeValue(event){
           let value = event.target.value;
           if(this.titleModifiers.capitalize){
               value = value.charAt(0).toUpperCase() + value.slice(1);
           }
           this.$emit('update:title', value)
       },
   }
}

生命周期函数名称变更

beforeDestory -> beforeUnmount

destoryed -> unmounted

Proxy


1. 支持监听对象属性的新增和变更

2. 支持监听数组长度和下标元素的变更

新增内置组件

Teleport 瞬间移动

允许将其内部包裹的元素移动到指定的目标元素, 其自身有两个props : todisabled

  • to:string , 必须是有效的查询选择器或 HTMLElement
<!-- 正确 -->
<teleport to="#some-id" />
<teleport to=".some-class" />
<teleport to="[data-teleport]" />

<!-- 错误 -->
<teleport to="h1" />
<teleport to="some-string" />
  • disabled:boolean, 禁用 的功能,使其插槽内部的元素无法移动
<teleport to="#some-id" :disabled=“isCanTeleport” />

同时, 多个 <teleport> 组件可以将其内容挂载到同一个目标元素, 按先后顺序追加

示例: Modal的使用
<template>
  <div>
    <teleport to="#modal-container">
      <div id="center" v-if="isOpen">
         <h1>this is a modal</h1>
             <button @click="buttonClick">Close</button>
      </div>
    </teleport>
  </div>
</template>

<script lang="ts">
import Vue, { defineComponent } from 'vue'
export default defineComponent({
  props:{
    isOpen:Boolean,
  },
  emits:{
    'close-modal': null
  },
  setup(props,context){
    const buttonClick = ()=> {
      context.emit('close-modal')
    }
    return {
      buttonClick
    }
  }
})
</script>

<style scoped>
  #center {
    width: 200px;
    height: 200px;
    border: 2px solid black;
    background: white;
    position: fixed;
    left: 50%;
    top: 50%;
    z-index: 111;
    margin-left: -100px;
    margin-top: -100px;
  }
</style>


//在App.vue中
<template>
  <div>
    <Modal :isOpen="modalIsOpen" @close-modal="onModalClose" > My Modal !!!!</Modal>
    <button @click="openModal">Open Modal</button><br/>
  </div>
</template>

//index.html中
<body>
    <div id="app"></div>
    <div id="modal-container"></div>
    <!-- built files will be auto injected -->
</body>
Suspense 异步请求帮手

vue3.0中, 不单单提供了defineAsyncComponent方法来生成异步组件, 同时也提供了一个内置组件<Suspense>, 其主要有两个插槽defaultfallback分别来展示不同场景下的数据:

1.default : 异步请求后需要显示的数据

2.fallback : 异步请求之前的占位数据 
定义一个异步组件 AsyncShow,在 setup 返回一个 Promise ,如下:
<template>
    <h1>{{result}}</h1>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
   setup(){ 
       return new Promise((resolve) => {
            setTimeout(()=> {
                return resolve({
                    result:42
                })
            },3000)
       })
   }
})
</script>

在App.vue中使用:

<Suspense>
  <template #default>
    <async-show />
  </template>
  <template #fallback>
    <h1>Loading !...</h1>
  </template>
</Suspense>

于此同时, 如果异步组件中数据请求错误时, 在父组件中也可以使用setuponErrorCaptured 实现错误数据捕获

//app.vue
export default {
    setup(){
        const error = ref(null)
        onErrorCaptured((e) => {
            error.value = e
        })
        
        return {
            error
        }
    }
}

效果如下:

2.2 Composition API

setup

setup是vue3.0提供的一个新的组件选项, 作为组件内部使用组合API的入口点. 在创建组件实例时,在初始prop 解析之后立即调用 setup。在生命周期方面,它是在 beforeCreate 钩子之前调用的

  • 参数

该函数将接收到的 props 作为其第一个参数:

export default {
  props: {
    name: String
  },
  setup(props) {
    console.log(props.name)
  }
}

需要注意, 此props 对象是响应式的, 即在传入新的props时会对其进行更新, 并且watchEffectwatch 进行观测和响应

export default {
  props: {
    name: String
  },
  setup(props) {
    watchEffect(() => {
      console.log(`name is: ` + props.name)
    })
  }
}

但是,不要对props对象进行解构使用,否则它会失去响应式:

export default {
  props: {
    name: String
  },
  setup({ name }) {
    watchEffect(() => {
      console.log(`name is: ` + name) // 没有响应式
    })
  }
}

该函数将接收到的 context 作为其第二个参数, 该上下文对象包含三个属性: attrsslotsemit

1. attrs: 即组件外部传入的但未在props中定义的属性, 和2.x的this.$attrs作用相同, 且不可以使用es6解构

2. slots: 即组件插槽, 和2.x的this.$slots作用相同, 且不可以使用es6解构

3. emit: 即组件对外暴露事件, 和this.$emit作用相同
  • 返回值

如果 setup 返回一个对象, 则该对象的属性将合并导组件模版渲染的上下文中, 和data的使用和作用一致

<template>
  <div>{{ count }} {{ object.foo }}</div>
</template>

<script>
  import { ref, reactive } from 'vue'

  export default {
    setup() {
      const count = ref(0)
      const object = reactive({ foo: 'bar' })

      // 暴露到template中
      return {
        count,
        object
      }
    }
  }
</script>

请注意,从 setup 返回的 refs 在模板中访问时会自动展开,因此模板中不需要 .value

渲染函数/JSX 的方法

同时, setup还允许返回一个渲染函数, 该函数可以直接使用在同一作用域中声明的反应状态

import { h, ref, reactive } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const object = reactive({ foo: 'bar' })

    return () => h('div', [count.value, object.foo])
  }
}
  • 注意
不要在 setup 中使用 this, 在其内部获取到的this和真正使用的关键字this是不同的, 正常来说, 参数props,context已经满足基本的日常使用
Ref

接受一个初始数据并返回一个响应式可变ref对象. ref对象具有指向数据的属性.valiue,且在模版中使用时, 可以无需展开value, 直接使用

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="increase">增加</button>
  </div>
</template>


<script lang="ts">

import { ref } from "vue";

export default {

    setup(){ 
       const count = ref(0);
       const increase = ()=> {count.value ++};
       return {
           count,
           increase
       }
    }
}
</script>
  • 类型声明
interface Ref<T> {
  value: T
}

function ref<T>(value: T): Ref<T>

当在日常项目开发中, 需要进一步指定ref对象的数据类型,可以通过以下方式:

const foo = ref<string | number>('foo') // foo's type: Ref<string | number>

foo.value = 123 // ok!

foo.value = true // error!

如果使用泛型的类型未知, 则建议将ref 装换为Ref<T>

  function getValue<T extends number>(data:T){
     const value = ref(data) as Ref<T>
     return value;
  }
Reactive

接受一个初始对象并返回一个响应式可变的对象,且该对象不可以解构, 否则会丧失其响应式

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="increase">增加</button>
  </div>
</template>


<script lang="ts">

import { reactive } from "vue";

export default {

    setup(){ 
       const data = reactive({
           count: 0,
           increase: () => { data.count++}
       })
       
       return data;
    }
}
</script>

由于 reactive返回的对象没有明确的指向其类型, 则为any类型, 那么在日常项目中, 为了严谨考虑, 我们可以单独指明其数据类型:

interface DataProps {
  count: number;
  increase: () => void;
}

export default {

    setup(){ 
       const data: DataProps = reactive({
           count: 0,
           increase: () => { data.count++}
       })
       
       return data;
    }
}

由于对 reactive 生成的对象进行解构使用会丧失数据的响应式, 但日常开发过程中, 难免会遇到对应的需求, 这时,我们可以引入toRefs对该对象进行转化, 它会对其内部数据转化成一个响应式可变的ref对象

   setup(){ 
       const data: DataProps = reactive({
           count: 0,
           increase: () => { data.count++}
       })
       
       const refData = toRefs(data)
       
       return {
         ...refData 
       };
    }
computed

使用computed来计算数据属性的时候, 有两种参数方式:单独的getter函数带有 get 和 set 函数的对象, 示例如下:

  • 接受一个getter函数, 并返回一个不可变的ref响应式对象
<template>
  <div>
    <h1>{{count}}</h1>
    <h1>{{double}}</h1>
    <button @click="increase">增加</button>
  </div>
</template>


<script lang="ts">

import { ref,computed } from "vue";

export default {

    setup(){ 
       const count = ref(0);
       const increase = ()=> {count.value ++};
       const double = computed(()=> {
           return count.value * 2
       });
       return {
           count,
           increase,
           double
       }
    }
}
</script>

  • 使用具有 get 和 set 函数的对象来创建可写的 ref 对象
export default {

    setup(){ 
       const count = ref(0);
       const double = computed({
           get:()=> count.value * 2,
           set: value => {
               count.value = value - 2
           }
       });
       
       double.value = 2;
       console.log(count.vaule); // 0
       
       return {
           count,
           double
       }
    }
}

类型声明

1. function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>

const num = computed<number>(() => {
   return 2 * 2; // ok!
   //return 2 + '1'; // error!
})


2. function computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>

const num = computed<number>({
    get: () => 2 * 2,
    set: (value:number) => {
          let a = value - 1
    }
})

watchEffect

立即运行一个函数并响应式跟踪其内部响应式依赖对象,其在其依赖项发生改变,重新运行该函数


import { ref, reactive , watchEffect } from "vue";

export default {

    setup(){ 
       const count = ref(0);
       const data: DataProps = reactive({
         title: '标题一',
       })
        
       const change = () => {
           data.title = '更新标题';
           count.value ++
       }
       
       // 当count 或者 data.title数据发生变动时, 会重新执行
       watchEffect(()=> {
           console.log('数据发生了变化',count.value,data.title)
       })

       return {
           count,
           double,
           change
       }
    }
}
  • 停止监听

watchEffect 在组件的setup()函数或者生命周期函数被调用时, 侦听器会被自动链接到该组件的生命周期,并且在组件卸载时自动停止.

在某种情况下, watchEffect会返回一个函数对象, 可以手动停止侦听:

const stop = watchEffect(() => {
  /* ... */
})

stop()

同时, 其函数接收一个onInvalidate函数入参, 用来注册清理失效时的回调. 而函数调用时机为:

1. 副作用函数重新执行

2. 侦听器被停止(手动停止或者组件卸载时)
export default {

    setup(){ 
       const count = ref(0);
       const data: DataProps = reactive({
         title: '标题一',
       })
        
       const change = () => {
           data.title = '更新标题';
           count.value ++
       }
       
       // 当count 或者 data.title数据发生变动时, 会重新执行
       const stop = watchEffect(()=> {
            onInvalidate(()=> {
               console.log('数据失效了')
            })
           console.log('数据发生了变化',count.value,data.title)
       })
       
       setTimeout(()=>{
          stop()
       },500)

       return {
           count,
           double,
           change
       }
    }
}

  • 侦听器调试

onTrackonTrigger 选项可用于调试侦听器的行为, 且只在开发模式下工作

1. onTrack: 响应式property 或 ref 作为依赖项被追踪时调用

2. onTrigger: 当依赖项变更导致副作用被触发时调用
watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)
watch

在默认情况下, watch是惰性的, 即只有当被侦听的数据源发生了变化吗,才会执行回调

watchEffect 比较, watch 更加具体:

1. 懒执行副作用函数

2. 更具体的说明什么状态下应该触发侦听器重新执行

3. 可以访问状态变化前后的数据

且同时共享一些行为:

1. 共享停止侦听

2. 清除副作用

3. 副作用的刷新时机

4. 侦听器调试 (onTrack , onTrigger), 只在开发模式下工作
  • 单一数据源侦听

数据源对象: ref对象、返回值的getter函数

import { ref, reactive , watch } from "vue";

export default {

    setup(){ 
       const count = ref(0);
       const data: DataProps = reactive({
         title: '标题一',
       })
        
       const change = () => {
           data.title = '更新标题';
           count.value ++
       }
       
       //侦听ref
       watch(count,(newValue,oldValue) => {
           console.log(`新数据:`,newValue);
           console.log(`旧数据:`,oldValue);
       })
       
       //侦听getter函数
       watch(()=> data.title,(newValue,oldValue)=> {
           console.log(`新数据:`,newValue);
           console.log(`旧数据:`,oldValue);
       })
       
       return {
           count,
           double,
           change
       }
    }
}
  • 多个数据源侦听

以数组的方式容纳多个ref对象或get函数 , 且回调函数并以数组的方式,返回对应的数据变化:

import { ref, reactive , watch } from "vue";

export default {

    setup(){ 
       const count = ref(0);
       const data: DataProps = reactive({
         title: '标题一',
       })
        
       const change = () => {
           data.title = '更新标题';
           count.value ++
       }
       
       //侦听ref、get函数
       watch([count,()=> data.title],(newValue,oldValue) => {
           console.log(`新数据:`,newValue);  //数组
           console.log(`旧数据:`,oldValue); // 数组
       })
       
       //或者
       
       watch([count,()=> data.title], ([newCount,newTitle],[oldCount,oldTitle]) => {
          console.log(`新数据:`,newCount,newTitle); 
          console.log(`旧数据:`,oldCount,oldTitle);
       })
       
       return {
           count,
           double,
           change
       }
    }
}