一、你好,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 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的
2. 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-if 和 v-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 : to、disabled
- 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>, 其主要有两个插槽default 和 fallback分别来展示不同场景下的数据:
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>
于此同时, 如果异步组件中数据请求错误时, 在父组件中也可以使用setup 的 onErrorCaptured 实现错误数据捕获
//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时会对其进行更新, 并且watchEffect 和 watch 进行观测和响应
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 作为其第二个参数, 该上下文对象包含三个属性: attrs 、slots、emit
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
}
}
}
- 侦听器调试
onTrack
和 onTrigger 选项可用于调试侦听器的行为, 且只在开发模式下工作
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
}
}
}