Vue3快速入门(二)

此文紧接Vue3快速入门(一)。

TypeScript

基本类型

前面我们学了vue3的基础语法,接下来我们再学一些typescript的语法,后面的vue都会基于typescript讲解。

typescript可以理解为就是加了类型标注的Javascript。

这样就可以在写代码的时候帮助们进行代码的校验,主要是数据格式的校验,不过最终typescript的代码还是需要转成JavaScript,这样才能在浏览器中运行,转换暂时不用考虑,vue开发环境支持编译typescript,主要目的是让我们的代码更加的规范。

之前的代码大家可以在下面网址中进行测试,可以试试看到ts编译后的js是什么样的。

网址:https://www.typescriptlang.org/zh/play

类型标注

typescript中我们可以给变量标记类型,后续的代码中 ts会自动校验变量是否有错误的读/写操作

let is:boolean;
is =  true;
is =  123// 报错, 提示number类型不能赋值给boolean类型的变量

语法很简单,就是在变量后面加上”:“和 类型,这个动作叫 类型标注

类型自动推断

如果变量的值是一个字面量,ts可以自动推断出类型

let is = false;
is = true;
is =  123// 报错, 提示number类型不能赋值给boolean类型的变量

let o = {a:1,b:'2'}
o.a ='33' // 报错, 不能把string分配给number

字面量

字面量就是数据的表现形式,我们看到就知道这个数据的内容,比如上面”=”右侧的数据。

本身概念很简单,单独提出只是怕有人第一次听到这个词的会有疑惑。

其他情况

还有几种情况ts会自动推断变量类型,下面我们再展开讲。

基础数据类型

接下来我们看看都有哪些数据类型。

boolean

布尔类型

let is:boolean;
is =  true;
is =  123// 报错, 提示is是数字不能赋值给boolean类型的变量

number

数字类型,不仅仅支持10进制.

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
decLiteral = '123'// 报错

string

字符串类型

let s1:string = 'hello world!'
let s2:string = `hello ${name}`;
s1 = 123 // 报错

数组

数组类型有2种表示方式

“类型+[]”
let numbers:number[] = [1,2,3,4,5];

// number|string代表联合类型, 下面的高级类型中会讲
let numbers:(number|string)[] = [1,2,3,4,'5'];
Array<类型>
let numbers:Array<number> = [1,2,3,4,5];

元组(Tuple)

元组类型表示一个已知元素数量类型数组,各元素的类型不必相同:

let list1:[number, string] = [1'2'3]; // 错误, 数量不对, 元组中只声明有2个元素
let list2:[number, string] = [12]; // 错误, 第二个元素类型不对, 应该是字符串'2'
let list3:[number, string] = ['1'2]; // 错误, 2个元素的类型颠倒了
let list4:[number, string] = [1'2']; // 正确

枚举(enum)

枚举是ts中有而js中没有的类型,编译后会被转化成对象,默认元素的值从0开始,如下面的Color.Red的值为0,以此类推Color.Green为1,Color.Blue为2:

enum Color {Red, Green, Blue}
// 等价
enum Color {Red=0, Green=1, Blue=2}

我们还可以反向通过值得到键:

enum Color {Red=1, Green=2, Blue=4}
Color[2] === 'Green' // true

看下编译成js后的枚举代码,你就明白为什么可以反向得到键值:

var Color;
(function (Color{
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
// Color的值为: {0: "Red", 1: "Green", 2: "Blue", Red: 0, Green: 1, Blue: 2}

any(任意类型)

any代表任意类型,也就是说,如果你不清楚变量是什么类型,就可以用any进行标记,比如引入一些比较老的js库,没有声明类型,使用的时候就可以标记为any类型,这样ts就不会提示错误了,当然不能所有的地方都用any,那样ts就没有使用的意义了。

void

void 的意义和 any 相反,表示不是任何类型,一般出现在函数中,用来标记函数没有返回值:

function abc(n:number):void{
    console.log(n);
}

void类型对应2个值,一个是undefined,一个null:

const n1:void = undefined;
const n2:void = null;

null 和 undefined

默认情况下null和undefined是所有类型的子类型,比如:

const n1:null = 123;
const n2:undefined = '123';

never

never 表示不可达,用在throw的情况下:

function error():never{
    throw '错了!';
}

unknown

unknown表示未知类型,他和any一样的地方是标记后的变量可以随意赋值,但是在访问变量上的属性的时候要先断言变量的类型才可以操作。

let a:unknown;
a = '汉字';

(a as string).length

// 等价写法
(<string>a).length

2种语法效果一样, 个人建议大家使用第一种, 书写更容易。

symbol

let s:symbol;
s = Symbol('a');
s = 1 //报错 

object

object表示非原始类型,也就是除number/ string/ boolean/ symbol/ null/ undefined之外的类型:

let o1:object = [];
let o2:object = {a:1,b:2};

但是, 我们实际上基本不用object类型的, 因为他标注的类型不具体, 一般都用接口来标注更具体的对象类型, 后面们讲”如何定义接口”。

类型保护(自动类型推导)

上节我们说ts可以从”字面量”推断出变量类型,ts的这种”自动推断”的能力叫做”类型保护“。

let o = {a:1,b:'2'// 可以识别对象中的字段和类型
o.a ='33' // 报错, 不能把string分配给number

typeof

判断类型,会触发”类型保护“。

let n = 0.5 < Math.random()? 1:'1';

// 如果没有typeof, n*=2会报错, 提示没法推断出当前是number类型, 不能进行乘法运算
if('number' === typeof n) {
    n*= 2;
else  {
    n= '2';
}

instanceof

判断是否实例,会触发”类型保护“。

let obj = 0.5 < Math.random() ? new String(1) : new Array(1);

if(obj instanceof String){
    // obj推断为String类型
    obj+= '123'
else {
    // obj为any[]类型
    obj.push(123);
}

in

判断是否存在字段,会触发”类型保护“。

interface A {
  x: number;
}

interface B {
  y: string;
}

function ab(q: A | B{
  if ('x' in q) {
    // q: A
  } else {
    // q: B
  }
}

自定义类型保护(is)

前面我们都是用系统自带的关键词触发”类型保护”,下面我们自定义一个:

  1. 首先我们需要定义一个函数,用来判断并返回boolean值。
  2. 返回值不要直接标注boolean,而是使用”is”,is前面是参数,后面是判断后的类型。
  3. 比如下面的”isBird”函数,如果返回值是true,那么”animal”就是Bird。
  4. 配合”if”使用”isBird”时,会触发自动类型推断。
interface Animal {
    name: string;
}

interface Bird {
    name: string;
    hasWing: boolean;
}

// 关键看函数返回值的类型标注
function isBird(animal:Animal): animal is Bird {
    return animal.name.includes('雀');
}


// 使用isBird对触发自动类型推断
const who = { name'金丝雀' };
if (isBird(who)) {
    who.hasWing = true;
}

is在vue3源码中应用

了解即可。

// 是否是对象
export const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 是否ref对象
export function isRef(v: any): v is Ref {
  return v ? v[refSymbol] === true : false
}

// 是否vnode
export function isVNode(value: any): value is VNode {
  return value ? value._isVNode === true : false
}

// 是否插槽节点
export const isSlotOutlet = (
  node: RootNode | TemplateChildNode
): node is SlotOutletNode =>
  node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.SLOT

高级类型

高级类型其实就是”基本类型”的各种组合形式。

接口(interface)

一种定义复杂类型的格式,比如定义对象类型

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite: string;
}

const article: Article = {
    title'为vue3学点typescript(2), 类型',
    count9999,
    content'xxx...',
    fromSite'baidu.com'
}

// 报错不存在titleA字段, 同时缺少title/count/content/fromSite
const article: Article = {
    titleA'为vue3学点typescript(2), 类型',
}

当我们给article赋值的时候,如果任何一个字段没有被赋值或者字段对应的数据类型不对,ts都会提示错误,避免字段名拼写错误或漏写。

扩展性

有时对象的字段可能是允许未知的,比如Article可能还有”a”字段”b”字段,为了兼容这种情况,我们改造下:

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite: string;
   [k:string]:string;
}

通过”[k:string]:string“我们允许Article类型中出现任意字符串键值,只要保证内容也为字符串即可。

非必填(?)

用”?”标记fromSite字段为非必填。

interface Article {
    title: string;
    count: number;
    content:string;
    fromSite?: string; // 非必填
}

// 缺少fromSite字段也不会报错
const article: Article = {
    title'为vue3学点typescript(2), 类型',
    count9999,
    content'xxx...',
}

只读(readonly)

用”readonly”标记字段不可修改。

interface Article {
    readonly title: string;
    count: number;
    content:string;
    fromSite?: string; // 非必填
}

let a:Article =  {title:'标题',count:1,content:'内容',}
a.title = 123// 报错, title是只读属性

定义函数

// 声明接口
interface Core {
    (n:number, s:string):[number,string]
}

// 使用接口进行标注
const core:Core = (a,b)=>{
    return [a,b];
}

函数也是对象,所以接口Core还可以声明属性。

interface Core {
    (n:number, s:string):[number,string];
  n:number;
}

const core:Core = (a,b)=>{
    return [a,b];
}
core.n = 100;

继承

让一个接口拥有另一个接口的所有特性。

interface Animal {
    color: string;
}

interface Cat extends Animal {
    foot: number;
}

let cat:Cat;
cat.color = "blue";
cat.foot = 10;

合并

相同名字的接口会自动合并。

interface Apple {
    color: string;
}
interface Apple {
    weight: number;
}

等价于

interface Apple {
    color: string;
    weight: number;
}

类型别名(type)

通过”type”关键字可以给已有的类型换个名字。

type N = number;
type S = string;

interface F1{
 (a:number):string;
}
type F2 = F1;

定义函数

前面说过接口可以定义函数,type也可以,但是语法有一点不一样:

interface F1{
 (a:number):string;
}

type F2 = (a:number)=>string;

“F1″和”F2″定义的是相同的类型,主要参数和返回值中间的他俩的区别,一个是”:”一个是”=>”。

乍一看,你一定觉得这用处不大,而且定义函数”interface”也是可以实现的,那么接下来讲下他的特别之处。

定义字面量类型

这是”type”能做到而”interface”不能实现的。

type N = 1|2|3;
type S = 'a'|'b'|'c';

// 正确
const n1:N = 1;
// 错误, 类型N中没有11;
const n2:N = 11

联合类型( | )

把多个类型用”|“连接到一起,表示类型之间”或”的关系。

type F1 = (a:string|number)=>any;
const f1:F1 = (a)=>a;

// 正确
f1(1);
f1('A');

// 错误, 参数类型应该是string或number
f1({a:1});
interface Cat  {
   hand:string;
    foot: string;
}

interface Duck{
 body:string;
}

type Animal  = Cat|Duck;

// 错误, Cat没有body字段
const animal:Cat = {hand:'手',body:'身体',foot:'脚'};
// 正确
const animal:Animal = {hand:'手',body:'身体',foot:'脚'};

交叉类型( & )

把多个类型用”&“连接到一起,取类型的并集。

interface A {a:number};
interface B {b:string};
type AB = A & B;

const a:A = {a:1};
const b:B = {b:'1'};
// 错误, 缺少a字段
const ab1:AB = {b:'2'};
// 正确
const ab2:AB = {a:1,b:'2'};

泛型

泛型可以理解为使用”不确定的类型”去描述类/函数/接口,而在使用的时候再确定具体的类型。

这个”不确定的类型”我们叫他类型变量,可以表示任意类型,一般我们用大写字母表示类型,比如”T“或”U“。

function echo<T>(input:T):T{
    return input;
}

泛型函数

在上面我们定义了一个”泛型函数”,首先定义了类型变量T,用”<>”包围, 然后标注函数的参数和返回值的类型都是”T”。

虽然使用”echo”的时候参数”input”可以是任意类型,但是不同于标注”any”类型,这里函数的返回值会根据参数类型的变化而同步变化。

// n1是number类型
const n1 = echo<number>(1);

// s会推断为string类型
const s = echo<string>('1'

如果参数是字面量,那么使用函数的时候可以省略前面的”<number>“和”<string>“,ts可以自动推断出参数的类型,从而推断出类型变量T的值:

// n1是number类型
const n1 = echo(1);

// s会推断为string类型
const s = echo('1'

泛型类

在类名后面通过”<>”声明一个类型变量U,类的方法和属性都可以用这个U类型。

class Person<U{
    who: U;
    
    constructor(who: U) {
        this.who = who;
    }

    say(code:U): string {
        return this.who + ' :i am ' + code;
    }
}

接下来我们使用下泛型类

let a =  new Person<string>('詹姆斯邦德');
a.say(007// 错误, 会提示参数应该是个string
a.say('007'// 正确

我们指定类型变量为(string),告诉ts这个类的U是string类型,通过Person的定义,我们知道say方法的参数也是string类型,所以a.say(007)会报错,因为007是number,所以我们可以通过传入类型变量来约束泛型。

泛型方法

和泛型函数的定义方式一样:

class ABC{
    // 输入T[], 返回T
    getFirst<T>(data:T[]):T{
        return data[0];
    }
}

泛型类型

我们可以用类型变量去描述一个类型,这里可以结合type和interface实现:

type
type A<T> = T[];
// 正确
const a: A<number> = [1,2,3]
// 错误
const b: A<number> = ['1','2','3'];
泛型接口(interface)
interface Goods<T>{
    id:number;
    title: string;
    size: T;
}

// 正确
let apple:Goods<string> = {id:1,title'苹果'size'large'};
let shoes:Goods<number> = {id:1,title'苹果'size43};
默认值
type A<T=string> = T[];
// 正确
const a:A  = ['1','2','3'];

interface Goods<T=string>{
    id:number;
    title: string;
    size: T;
}
// 正确
let apple:Goods<string> = {id:1,title'苹果'size'large'};

泛型约束

function echo<T>(input: T): T {
    console.log(input.name); // 报错, T上不确定是否由name属性
    return input;
}

前面说过T可以代表任意类型,但并不是所有类型上都有”name“字段,通过”extends“可以约束”T”的范围:

// 现在T是个有name属性的类型
function echo<T extends {name:string}>(input: T): T {
    console.log(input.name); // 正确
    return input;
}

多个类型变量

可以同时使用多个类型变量。

function test<T,U>(a:T,b:U):[T,U]{
 return [a,b];
}

不滥用泛型

泛型主要是为了约束,或者说缩小类型范围,如果不能约束功能,就代表不需要用泛型:

function convert<T>(input:T[]):number{
    return input.length;
}

这样用泛型就没有什么意义了,和any类型没有什么区别。

工具类型

现在介绍下系统预设的工具类型。

Partial

让属性都变成可选的

type A  = {a:number, b:string}
type A1 = Partial<A> // { a?: number; b?: string;}

Required

让属性都变成必选

type A  = {a?:number, b?:string}
type A1 = Required<A> // { a: number; b: string;}

Pick<T,K>

只保留自己选择的属性,K代表要保留的属性键值

type A  = Pick<{a:number,b:string,c:boolean}, 'a'|'b'>
type A1 = Pick<A, 'a'|'b'//  {a:number,b:string}

Omit<T,K>

实现排除已选的属性

type A  = {a:number, b:string}
type A1 = Omit<A, 'a'// {b:string}

Record<K,T>

创建一个类型,K代表键值的类型,T代表值的类型

type A1 = Record<string, string> // 等价{[k:string]:string}

Exclude<T,U>

过滤T中和U相同(或兼容)的类型

type A1 = Exclude<number|string, string|number[]> // number

// 兼容
type A2 = Exclude<number|string, any|number[]> // never , 因为any兼容number, 所以number被过滤掉

Extract<T,U>

提取T中和U相同(或兼容)的类型

type A1 = Extract<number|string, string|number[]> // string

NonNullable

剔除T中的undefined和null

type A1 = NonNullable<number|string|null|undefined// number|string

ReturnType

获取T的返回值的类型.

type A1= ReturnType<()=>number> // number

InstanceType

返回T的实例类型

ts中类有2种类型,静态部分的类型和实例的类型,所以T如果是构造函数类型,那么InstanceType可以返回他的实例类型

interface A{
    a:HTMLElement;
}

interface AConstructor{
    new():A;
}

function create (AClass:AConstructor):InstanceType<AConstructor>{
    return new AClass();
}

Parameters

获取函数参数类型, 返回类型为元组,元素顺序同参数顺序

interface A{
    (a:number, b:string):string[];
}

type A1 = Parameters<A> // [number, string]

ConstructorParameters

获取构造函数的参数类型,和Parameters类似,只是T这里是构造函数类型

interface AConstructor{
    new(a:number):string[];
}

type A1 = ConstructorParameters<AConstructor> // [number]

更多

其实你可以在**”**node_modules/typescript/lib/lib.es5.d.ts“中找到上面所有类型的定义,随着ts的更新,这里可能会有更多的类型。

Vue3快速入门(二)

自定义工具类型(keyof/extends/in/infer)

上小节我们学了很多系统预设的工具类型,接下来我们学习自己实现工具类型。

这小节的内容有些”进阶”,在初级阶段大家选学,实际工作中多练习即可掌握。

keyof

先看系统预设的Partial的源码

type Partial<T> = {
    [P in keyof T]?: T[P];
};

这里出线了2个新的关键词”keyof“和”in“,”keyof“的作用是取对象的所有键值,比如:

type A = keyof {a:string,b:number} // "a"|"b"

in

用来遍历联合类型

type A = "a"|"b"
type B = {
    [k in A]: string;
}
// B类型:
{
    a: string;
    b: string;
}

typeof(获取变量类型)

之前说的”typeof”是js中的api,实际在ts中也有”typeof”关键字,可以提取js对象的类型

function c(n:number){
  return n;
}

// typeof c获取函数c的类型
function d(callback :typeof c){
  const n = callback(1);
  console.log(n);
}

在vscode的提示中我们可以看到ts已经正确的推导出了”typeof c”的类型

Vue3快速入门(二)

extends(条件类型)

之前我们在”泛型约束“章节使用了”extends“用来约束类型范围,这里我们结合三目运算实现类型的条件判断。

语法为:

type A = T extends U ? X : Y;

如果U的范围大于T,比如U有的字段T中都有,那么A的值为X,反之为Y。

type T1 = {a:number,x:number};
type U1 = {a:number,b:number};
type A1 = T1 extends U1 ? number : string; // string

type T2 = {a:number,b:number};
type U2 = {a:number};
type A2 = T2 extends U2 ? number : string; // number

系统预设类型中的”Exclude”也是如此实现:

type Exclude<T, U> = T extends U ? never : T;

这里表示如果过滤掉T中有U中也有的类型:

type T1 = number|string;
type U1 = number|string[];
type A = Exclude<T1,U1> // string

infer(类型推断)

单词本身的意思是”推断”,这里表示在extends条件语句中声明待推断的类型变量。

先看下预设类型Parameters:

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

上面声明一个P用来表示…args可能的类型,如果(…args: infer P)范围大于T,那么返回…args对应的类型,也就是函数的参数类型,反之返回never。

注意:开始的T extends (…args: any) => any用来输入的T必须是函数

应用infer

接下来我们利用infer来实现”删除元祖类型中第一个元素”:

export type Tail<T extends unknown[]> = T extends [a: any, ...args: infer P] ? P : never

type A = Tail<[number,string,null]> // [string,null]

类型断言(as)

有些情况下系统没办法自动推断出正确的类型,就需要我们自行标记下:

document.body.addEventListener('click', e=>{
   // 报错, 因为e.target的类型是EventTarget, 其上没有innerHTML
    e.target.innerHTML = 'test'
  
    // 正确
    (e.target as HTMLElement).innerHTML = 'test';
});

因为”addEventListener“方法不仅仅dom元素支持,所以系统的类型描述中”e.target“不能直接标注成”HTMLElement“。

但是上面代码在使用的时候,我们就知道e.target是dom元素,所以就需要我们告诉ts:”这是dom”,这就是”类型断言”,语法为”as”,还可以用”<>”表示:

document.body.addEventListener('click', e=>{
    (e.target as HTMLElement).innerHTML = 'test';
   // 等价
   (<HTMLElement>e.target).innerHTML = 'test';
});

在axios中的应用

axios是开发最常用的http请求库,我们异步请求来的数据,他的类型一定是未知的,这就需要我们标注类型:

axios.get<{n:number}>('/xx').then(data=>{
    console.log(data.data.n+1);
});

注意”get”方法后面我们传入了”类型参数”,说明get方法在定义的时候使用了”泛型”. 在”<>”中传入的类型,表示的就是返回数据的类型,这里相当于标注返回值类型

但是假如你使用了”axios的拦截器”改变了返回值,那么在这里标注就不好使了,比如:


http.interceptors.response.use(function (response{
   // 让返回数据值返回data字段的值
   return data.data;
}, function (error{
    return Promise.reject(error);
});

由于我们修改返回值,但是axios并不知道他自己的返回值变了,所以还像上面那么标注返回值类型肯定不行了,这时我们就可以使用类型断言:

axios.get('/xx').then(data=>{
    console.log((data as {n:number}).n+1);
});

as const

这是一个特殊的断言,让从字面量推断出的范围更小:

const ab = [1,'2']; // 类型: (number|string)[]

前面讲过ts可以自动推断出类型,我们预期类型是[number,string],但实际并不是,所以如果你希望ts”更自信,那么我们用”as const“告诉ts可以按照最小范围进行推断:

const ab = [1,'2'as const// 类型: [number, string]

枚举(enum)

枚举是typescript自己的数据类型,javascript中并没有这个类型,但是这并没有兼容问题,因为在编译后枚举会被编译成js代码。

使用”enum”关键字定义枚举 ,可以把enum和Class的格式很像,成员用”=”赋值:

enum Direction {Up, Right, Down, Left};

看下编译后的js代码:

var Direction;
(function (Direction{
    Direction[Direction["Up"] = 0] = "Up";
    Direction[Direction["Right"] = 1] = "Right";
    Direction[Direction["Down"] = 2] = "Down";
    Direction[Direction["Left"] = 3] = "Left";
})(Direction || (Direction = {}));

从这里我们可以到作为枚举成员的值和键都被当做对象的键值进行了双向赋值,实际我们访问的就是这个对象,所以枚举支持双向访问:

Direction[0// 'Up'
Direction.Up // 0

应用场景

主要用来做常量的定义,枚举名相当于一个作用域,可读性更好些,同时ts对他的类型推断也更清晰。

值递增

枚举的成员的值如不指定,那么默认从0递增赋值:

Direction.Up // 0
Direction.Right // 1
Direction.Down // 2
Direction.Left // 3

如果某个成员被赋值了数字,那么从该数字递增:

enum Direction {Up=5, Right, Down=10, Left};
Direction.Up // 5
Direction.Right // 6
Direction.Down // 10
Direction.Left // 11

值只可以是number和string

// 正确
enum Direction {Up = '上', Right = 1};

// 错误
enum Direction {Up = ['上]', Right = {r:1}};

常量枚举(const)

使用”const enum”来声明常量枚举。

const enum Enum {
  A = 1,
  B = A * 2,
}

Enum.A
Enum.B

不同于普通的枚举,常量枚举在编译成js后,不会生成多余的代码

// 编译后的结果
"use strict";
1 /* A */;
2 /* B */;

开发中我个人更喜欢使用常量枚举,因为他不会生成多余的代码,同时还能保证良好的语义。

声明文件(declare)

声明文件分为全局变量声明类型,也可以为模块扩展变量声明。

什么是声明文件

如果ts项目中引入了js文件,那么就需要写”声明文件”来告诉ts引入js文件中的变量类型。

应用场景
  1. 如果在html中引入了第三方js库,比如”jq”,那么在ts代码中要使用”$“变量就需要”声明”,因为ts不知道$的存在。
  2. 对全局对象”window/doucument”等上缺少(或自定义)的字段进行补充。
  3. 已存在的npm中的js包,补充其类型声明。
  4. 对npm上已经存在的ts包的类型进行扩充,比如向vue实例上增加的$xx属性在ts中都需要自己声明才能使用。

举个例子

针对第一种情况实现代码如下:

// global.d.ts
declare var $: (selector: string) => any;

这里”declare“是声明的关键字,其他场景实例在后面讲解。

npm上的声明文件

像jq这种常见的js库,其实已经有人写好了声明文件,并发布到了npm中。

比如jq的声明文件的包是”@types/jquery”, lodash的声明文件是”@types/lodash”,如果你通过npm安装的包没有声明文件你可以执行对应的安装命令:

npm i @types/xx
文件位置

声明文件一般放在项目根目录下,这样整个项目的ts文件都可以读取到声明的类型,命名格式为”xx.d.ts“。

全局interface / type

上面我们说是给变量增加类型声明,实际我们还可以在”global.d.ts”中添加接口和type,添加后接口就会变成全局的,这样在项目的任意文件中都可以读取到该类型。

// global.d.ts
interface Abc{
 n:number
}
// index.ts
const abc:Abc = {n:100};
引入(///)

如果你的想要复用已存在的类型,我们可以通过”///”语法去引入:

// test.d.ts
type ABC = number;
// global.d.ts
/// <reference path="./test.d.ts" />
declare const a:ABC // number

现在我们对声明文件有了初步的认识,下一节开始我们学习自己编写声明文件。

全局变量的声明

强调下声明文件我们一般放在项目的根目录,为了语义起名叫”global.d.ts“。

Vue3快速入门(二)
declare

对于全局变量的声明我们使用”declare“关键字实现,下面展示各种类型数据的声明写法。

declare var

声明全局变量

// global.d.ts
declare var n:number;
declare let s:string;
declare const version:string;

这里的”var”也可以是”let”或”const”,语义和js中的一致,根据情况可自行决定。

declare function

声明全局函数

// global.d.ts
declare function translate(words: string): string;
declare enum

声明全局枚举

// global.d.ts
declare enum Directions {
    Up,
    Down,
    Left,
    Right
}
declare class

声明全局类

// global.d.ts
declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}
declare namespace

声明命名空间, 可以理解为声明一个对象

// global.d.ts
declare namespace req {
    function get(url: string, params?: Record<string,any>): Promise<any>;
}

注意:本文声明的全局变量并不是window下的全局变量,而表示是当前项目所有代码作用域之上的一个变量

// global.d.ts
declare let a:number;
// index.ts
window.a = 1// ts报错, 提示没有a变量
声明Window下的变量
// global.d.ts
interface Window{
  a:number
}

注意:开头字母是大写”W“, interface有自动合并的功能, 所以在声明文件中直接对Window类型进行扩充

// index.ts
window.a = 1// 正确

扩展全局变量

上小节我们讲了全局变量Window的扩展,这小节我们再多讲几个技巧。

比如给”String”增加cut方法。

String.prototype.cut = function(n){
    return this.substr(0,n);
};

'12345'.cut(3); // 123

对应的声明文件:

// global.d.ts
interface String {
  // 自定义字段
  cut(s:string): string;
}

因为”String”类型是ts系统自带的声明文件中写好的,本身就是用interface描述的,还记得interface类型会自动合并的特性吧,这里直接写一个同名interface实现类型合并。

系统的声明文件

js的api在ts中都是有类型声明的,我们可以在项目的”node_modules/typescript/lib/“中看到所有的系统声明文件。

Vue3快速入门(二)
带浏览器前缀的API

我观察发现系统自带的声明,不会对带”浏览器前缀”的API进行声明,如果我们自己写插件就需要补充这部分,比如”requestFullScreen”这个api:

// global.d.ts
interface HTMLElement {
    webkitRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
    msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    mozRequestFullScreen(options?: FullscreenOptions): Promise<void>;
}
interface HTMLElement {
    webkitRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
    msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
    mozRequestFullScreen(options?: FullscreenOptions): Promise<void>;

    onwebkitfullscreenchange: ((this: Element, ev: Event) => any) | null;
    onmozfullscreenchange: ((this: Element, ev: Event) => any) | null;
    MSFullscreenChange: ((this: Element, ev: Event) => any) | null;
}

interface Document {
    readonly webkitFullscreenElement: Element | null;
    readonly msFullscreenElement: Element | null;
    readonly mozFullScreenElement: Element | null;

    webkitExitFullscreen(): Promise<void>;
    msExitFullscreen(): Promise<void>;
    mozCancelFullScreen(): Promise<void>;
}

扩展npm模块的类型(declare module)

npm下载的”包”自带了声明文件,如果我们需要对其类型声明进行扩展就可以使用”declare module”语法。

让vue3支持this.$axios
// main.ts
app.config.globalProperties.$axios = axios;

功能上我们实现了”this.axios字段,也就是在组件内使用this.$axios会提示缺少类型,所以添加如下声明文件:

// global.d.ts

import { ComponentCustomProperties } from 'vue'

// axios的实例类型
import { AxiosInstance } from 'axios'

// 声明要扩充@vue/runtime-core包的声明.
// 这里扩充"ComponentCustomProperties"接口, 因为他是vue3中实例的属性的类型.
declare module '@vue/runtime-core' {
  
  // 给`this.$axios`提供类型
  interface ComponentCustomProperties {
    $axios: AxiosInstance;
  }
}

这里扩充”ComponentCustomProperties“接口,因为他是vue3中实例的属性的类型。

更全面的例子

上面的例子中我们扩充了原声明中的interface,但是如果导出是一个Class我们该如何写呢?

下面我们对”any-touch”的类型进行扩充,这里”any-touch”的默认导出是一个Class。

假设我们对”any-touch”的代码做了如下修改:

  1. 导出增加”aaa”变量,是string类型。
  2. 类的实例增加”bbb”属性,是number类型。
  3. 类增加静态属性”ccc”,是个函数。
// global.d.ts

// AnyTouch一定要导入, 因为只有导入才是扩充, 不导入就会变成覆盖.
import AnyTouch from 'any-touch'

declare module 'any-touch' {
    // 导出增加"aaa"变量, 是个字符串.
    export const aaa: string;
  
    export default class {
      // 类增加静态属性"ccc", 是个函数.
      static ccc:()=>void
      // 类的实例增加"bbb"属性, 是number类型.
      bbb: number
    }
}

注意:AnyTouch一定要导入,因为只有导入才是类型扩充,不导入就会变成覆盖。

测试下,类型都已经正确的添加:

// index.ts
import AT,{aaa} from 'any-touch';

const s = aaa.substr(0,1);

const at = new AT();
at.bbb = 123;

AT.ccc = ()=>{};
对非ts/js文件模块进行类型扩充

ts只支持模块的导入导出,但是有些时候你可能需要引入css/html等文件,这时候就需要用通配符让ts把他们当做模块,下面是对”.vue”文件的导入支持(来自vue官方):

// global.d.ts
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
// App.vue
// 可以识别vue文件
import X1 from './X1.vue';
export default defineComponent({
 components:{X1}
})

声明把vue文件当做模块,同时标注模块的默认导出是”component”类型。

这样在vue的components字段中注册模块才可以正确识别类型。

vuex

下面是vuex官方提供的,在vue的实例上声明增加$store属性,有了前面的知识,看这个应该很轻松。

// vuex.d.ts

import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'

// 声明要扩充@vue/runtime-core包的声明
declare module '@vue/runtime-core' {
  // declare your own store states
  interface State {
    count: number
  }

  // provide typings for `this.$store`
  interface ComponentCustomProperties {
    $store: Store<State>
  }
}

说明

到此声明的内容就都结束了,但实际上还有些”模块声明”的方式并没有覆盖到,因为本文的最终目的是基于vue3开发应用并不涉猎npm包的开发,所以其他的内容就不展开了,有需要的小伙伴可以看ts文档来学习,有了本文的基础,相信你会很轻松学会更多:https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html

组合API(setup)

Vue3快速入门(二)

初识(setup/reactive/ref)

组合API“是vue3扩充的全新语法,前面基础文章中讲解的语法叫”选项API“。

简单概括就是vue3中多了一个”setup”字段,其内部可以以函数形式使用watch/computed等语法,下面例子简单看下结构即可, 至于他的优点等下会展开说明。

<template>
 <p>共消费: {{count}} </p>
 <button @click="changeCost">改价格</button>
</template>

<script lang="ts">
import { defineComponent,ref,computed,watch ,onMounted} from "vue";
export default defineComponent({
  name'Test',

 setup(){
    // 花费(对应data)
    const cost  = ref(5);
    
    // 总价格(对应computed)
    const count = computed(()=>{
      // 6元运费
   return cost.value + 6;
  });
    
    // 监视数据变化(对应watch)
    watch(cost,cost=>{
      alert('共消费'cost);
    })
    
    // 改价格(对应methods)
    function changeCost(){
     cost.value = 10;
    }
    
    // 返回数据和data一样, 模板可读取
    return {count, changeCost};
  }
});
</script>

优点

组合API“可以更进一步拆分”选项API“中的JS逻辑

可以把某一逻辑的”data/computed/watch/methods/声明周期钩子“单独封装到一个函数(也可单独一个文件)中。

一般给拆分后的函数命名”useXxx”。

Vue3快速入门(二)

拆分实际需求

分析下图的购物车模块,我计划把JS部分拆分成2部分(函数):

一个用来获取购物车商品数据和总价。

一个用来计算获取优惠劵并计算优惠后的总价。

分别是函数: “useGetCart“和”useCoupon“。

Vue3快速入门(二)

setup结构

拆成2个函数你一定很疑惑,老的”选项API”不是有”methods“字段也可以封装函数,暂不解释,先看下”组合API“的代码格式,一个新的字段”setup“,他是”组合API”的标志属性,是个函数,其返回值可以被模板识别并渲染,类似"data"

特别注意2个函数的返回值,他们返回了数据(类似data)和函数(类似methods)。

实际函数内部包含独立的"watch/computed/生命周期钩子",想当于把1个vue组件的”data”和”methods”的内容给分组了,这也就是为什么叫”组合API“。

<template>
  <div>
    {{ n }}
    <article v-if="cart.length > 0">
      <ul>
        <li v-for="item in cart" :key="item.name">
          {{ item.name }} : {{ item.price }}元
          <input v-model="item.count" type="number" style="width: 48px" />
        </li>
      </ul>
      <h5 style="margin-left: 100px">原价:{{ totalPrice }}元</h5>
      <h5 style="margin-left: 100px; color: #f10">
        总价:{{ realTotalPrice }}元
      </h5>
      <button @click="createOrder">支付</button>
    </article>
  </div>
</template>

<script lang="ts">
import {
  ref,
  Ref,
  computed,
  watch,
  defineComponent,
  onMounted,
  reactive,
from "vue";
export default defineComponent({
  name"Cart",

  setup() {
    const n = ref(110);
    console.log(n);
    const [cart, totalPrice, createOrder] = useGetCart();
    const realTotalPrice = useCoupon(totalPrice);
    return { cart, totalPrice, realTotalPrice, n, createOrder };
  },
});

/**
 * 获取购物车详情
 */

function useGetCart({
  //  购物车详情
  const cart = reactive<{ name: string; count: number; price: number }[]>([]);

  // 模拟异步请求
  setTimeout(() => {
    cart.push(
      { name"苹果"count10price10 },
      { name"香蕉"count20price20 }
    );
  }, 1000);

  // 总价格
  const totalPrice = computed(() => {
    return cart.reduce((total, item) => item.count * item.price + total, 0);
  });

  // 生成订单(methods)
  function createOrder({
    // 模拟生成订单
    setTimeout(() => {
      console.log(`成功购买${cart.length}件商品`);
    }, 1000);
  }

  return [cart, totalPrice, createOrder] as const;
}

/**
 * 获取优惠劵
 */

function useCoupon(totalPrice: Ref<number>{
  const realTotalPrice = ref(0);
  // 此处实际可以不用onMouted,
  // 仅仅为了演示用法
  onMounted(() => {
    // 模拟异步请求
    setTimeout(() => {
      const coupon = 9;
      watch(
        totalPrice,
        (value) => {
          realTotalPrice.value = value - coupon;
        },
        { immediatetrue }
      );
    }, 1000);
  });

  return realTotalPrice;
}
</script>

“useXxx”函数内部实现

这个”use”作为函数名前缀是一个命名习惯,实际起名并没有限制。

每一个函数中的"watch/computed/生命周期钩子", 他们都以函数的形式出现。

import {computed,reactive} from 'vue';
/**
 * 获取购物车详情
 */

function useGetCart({
  //  购物车详情(reactive)
  const cart = reactive<{ name: string; count: number; price: number }[]>([]);

  // 模拟异步请求
  setTimeout(() => {
    cart.push(
      { name"苹果"count10price10 },
      { name"香蕉"count20price20 }
    );
  }, 1000);

  // 总价格(computed)
  const totalPrice = computed(() => {
    return cart.reduce((total, item) => item.count * item.price + total, 0);
  });

  return [cart, totalPrice] as const;
}

这出现了一个新的函数”ref“,他是用来”定义响应数据”的,接下来我们就讲”ref”是什么。

注意:

  1. “as const”表示断言数组类型为元祖,如果大家忘记了ts部分的内容,在学习后面知识之前,可以温习下ts。

  2. 这里只有生命周期的钩子名字前面多了”on”前缀,比如mounted => onMounted。

定义响应数据(reactive/ref)

“响应数据”就是值变化可以驱动dom变化的数据,我们之前在”data“中定义的数据就是响应数据。

但是在”setup”中如果我们要定义数据,这里并没有"data"函数,取而代之的是”reactive/ref“函数:

reactive

定义响应数据,输入只能是对象类型,返回输入对象的响应版本。

<template>
    <h1>{{count}}</h1>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
 setup(){
    return reactive({count:99});
  }
});
</script>
Vue3快速入门(二)

实际这个例子中可以不用”reactive”,结果一样,但是如果”count”数据被修改,那么界面就不会自动变化了,一直显示”99″。

ref

同样是定义响应数据,和”reactive”的区别是返回值响应数据的格式不同,ref返回的数据需要用”.value”访问。

const n = ref(110);
console.log(n);
Vue3快速入门(二)

可以看到返回值在value字段中,这么做是因为js中对数据变化的监视只支持”引用数据类型”,对于string和number类型如果需要监视需要构造一个对象,所以这里ref内部就需要构造一个{value:110}的变量。

reactive和ref的选择

重要:

如果要监视的数据是引用型数据(object)那么就是用reactive。

如果是(number/boolean/string)等原始数据类型就用ref。

封装函数(useXxx)

现在我们回头看2个函数的实现。

useGetCart

返回购物车中商品信息和总价格,总价格使用了”计算属性”函数(computed),同时我们封装了”生成订单”函数,因为其需要购物车商品信息做参数,所以把2者做为一组。

import {computed} from 'vue';
/**
 * 获取购物车详情
 */

function useGetCart({
  //  购物车详情(reactive)
  const cart = reactive<{ name: string; count: number; price: number }[]>([]);

  // 模拟异步请求
  setTimeout(() => {
    cart.push(
      { name"苹果"count10price10 },
      { name"香蕉"count20price20 }
    );
  }, 1000);

  // 总价格(computed)
  const totalPrice = computed(() => {
    return cart.reduce((total, item) => item.count * item.price + total, 0);
  });
  
  
  // 生成订单(methods)
  function createOrder(){
   // 模拟生成订单
    setTimeout(()=>{
     console.log(`成功购买${cart.length}件商品`);
    },1000)
  }

  return [cart, totalPrice,createOrder] as const;
}

useCoupon

获取优惠金额,并返回计算优惠后金额。

用watch来监视”总价格”,当变化的时候重新计算”优惠后总价”,使用”onMounted”控制数据请求触发时机(本例并无实际意义,此处仅为了展示”onMounted”用法)。

import {watch,onMounted} from 'vue';
/**
 * 获取优惠劵
 */

function useCoupon(totalPrice: Ref<number>{
  const realTotalPrice = ref(0);
  // 此处实际可以不用onMouted,
  // 仅仅为了演示用法
  onMounted(() => {
    // 模拟异步请求
    setTimeout(() => {
      const coupon = 9;
      watch(
        totalPrice,
        (value) => {
          realTotalPrice.value = value - coupon;
        },
        { immediatetrue }
      );
    }, 1000);
  });

  return realTotalPrice;
}

“watch”作为函数,其第二个参数是个对象,有字段”immediate”表示初始化即运行回调,”deep”表示深度监视数据(object)。

setup的参数(props/emit)

因为setup的解析在其他组件选项之前,所以setup中”this”并不是组件实例,之前通过”this”获取的属性现在都要从setup的参数中获取,比如”props/emit”。

props

setup的第一个参数就是props。

<!--App.vue-->
<Test :number="100"/>
<!--Test.vue-->
<template>
  <p>{{number}} </p>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name'Test',
  
  props:{
   number:{
     type:Number
    }
  },

  setup(props){
    console.log(props.number); // 100
  }
});

emit

setup的第二个参数是个对象,包含”attrs/slots/emit/expose”,这里包含4个属性,但是这里我们只讲emit。

其他3个在业务代码开发中并不常用, 本文的出发点是帮助大家快速上手开发,所以关于他们的内容大家暂时可以自行官网了解。

<!--Child.vue, 子组件-->
<template>
  <div>
    <p>{{ number }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name"Child",

  props: {
    number: {
      typeNumber,
    },
  },

  setup(props, { emit }) {
    // 模拟接口请求
    setTimeout(() => {
      emit("update:number", props.number + 1);
    }, 1000);
  },
});
</script>

<!--Parent.vue, 父组件-->
<template>
  <Child v-model:number="n" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import Child from "./components/Child.vue";
export default defineComponent({
  components: { Child },
  data() {
    return { n99 };
  },
});
</script>
Vue3快速入门(二)

上面我们实现了setup模式下的”v-model”。

setup中获取DOM元素

在非setup钩子中,我们都是通过”this.$refs“来获取指定元素。

但上小节我们说过setup中没有”this”,”props/emit”都是通过参数来获取,但是”$refs”并不存在于参数中。

setup中获取元素引用比较特殊,分3步骤:

  1. 定义一个ref变量,值为null。
  2. 通过”return”暴露ref变量。
  3. 把变量名赋值到元素的ref属性中。
<!--SetupRef.vue-->
<template>
 <!-- 第3步-->
  <h1 ref="titleRef">标题</h1>
</template>

<script lang="ts">
import { defineComponent,onMounted,ref } from "vue";
export default defineComponent({
  name"SetupRef",

  setup(){
    //  第1步
    const titleRef = ref(null);
    
    onMounted(()=>{
      console.log(titleRef.value);
    });
    
    // 第2步
    return {titleRef};
  }
});
</script>
Vue3快速入门(二)

特别强调:在模板中使用的是”ref”不是”:ref”。

<template>
  <h1 ref="titleRef">标题</h1>
</template>

什么时候用”:ref”?

当ref的值是一个函数的时候,我们必须用”:ref”,函数只有一个参数,那就是当前元素。

<template>
  <h1 :ref="getTitleRef">标题</h1>
</template>

<script lang="ts">
import { defineComponent,onMounted,ref } from "vue";
export default defineComponent({
  name"SetupRef",

  setup(){
    function getTitleRef(el:HTMLElement){
      console.log(el);
    }
    return {getTitleRef}
  }
});
</script>

结果同不带”:”的”ref”:

Vue3快速入门(二)

在”v-for”中获取多个ref

获取”单个元素(或者组件)的引用”用”ref”即可,但是如果需要获取循环中的引用,那么就只能使用”:ref”。同样需要3个步骤:

  1. 定义函数,函数需要一个参数,代表当前元素. 这个函数会被v-for循环多次,每次循环函数都可以获取到当前元素。
  2. 在setup的返回值中返回函数。
  3. 在模板中通过”:ref”指定函数。
<template>
  <!-- 第3步, 使用:ref-->
  <h1 v-for="n in 3" :key="n" :ref="getTitleRefs">标题{{ n }}</h1>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
export default defineComponent({
  name"SetupRefVFor",

  setup() {
    //  第1步, 定义函数
    function getTitleRefs(el: HTMLElement{
      console.log(el);
    }

    // 第2步, 返回函数
    return { getTitleRefs };
  },
});
</script>
Vue3快速入门(二)

computed和watch

在setup中computed和watch都以函数形式调用,所以要是用他们就需要单独引入,引入方法很简单:

import { computed, watch } from "vue";

computed

和”选项API”的语法类似,同样通过返回值来输出计算结果。

只是注意:其内部对”ref定义的变量”的计算依然需要通过”.value”来访问。

<template>
  <p>苹果单价: {{ price }}</p>
  <label>数量: <input
    v-model="num"
    type="number"
    style="padding: 8px; border: 1px solid #ccc"
  />
</label>
  <p>总价:{{ count }}</p>
</template>

<script lang="ts">
// computed要单独引入
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
  name"SetupComputed",

  setup() {
    const num = ref(10);
    const price = ref(5);
    // ⭐computed
    const count = computed(() => num.value * price.value);
    return { num, count, price };
  },
});
</script>
Vue3快速入门(二)

watch

语法:(data, callback, options )

data:被监视数据。

callback:数据变化后触发的函数,函数有2个参数,一个是”当前值”,一个是”变化前的值”。

options:是个对象,其中的”immediate“字段可以设置是否在数据变化前执行一次,还有”deep“字段表示数据是对象时,进行深度监控。注意:这里值列出2个参数,实际还有其他参数,由于并不常用,大家可自行官网了解。

stop:是个函数,用来停止对数据的监控。

<template>
  <div>
    <p>苹果单价: {{ price }}</p>
    <label
      >
数量:
      <input
        v-model="num"
        type="number"
        style="padding: 8px; border: 1px solid #ccc"
    />
</label>
    <p>总价:{{ count }}</p>
    <button @click="stop">stop</button>
  </div>
</template>

<script lang="ts">
import { watch, defineComponent, ref } from "vue";
export default defineComponent({
  name"SetupWatch",

  setup() {
    const num = ref(10);
    // 此处可以不用ref,
    // 因为值不需要改变(响应式)
    const price = 5;
    const count = ref(0);
    // ⭐watch
    const stop = watch(
      num,
      (num) => {
        count.value = price * num;
      },
      { immediatetrue }
    );

    return { num, count, price, stop };
  },
});
</script>

当我点击了”stop”,那么”总价”就不再根据”数量”变化了。

Vue3快速入门(二)

监控多个数据

通过watch函数,我们现在可以监视多个数据, 只需要把多个数据组成一个数组。

<template>
  <div>
    <label
      >
苹果单价:
      <input
        v-model="price"
        type="number"
        style="padding: 8px; border: 1px solid #ccc"
    />
</label>
    <label
      >
数量:
      <input
        v-model="num"
        type="number"
        style="padding: 8px; border: 1px solid #ccc"
    />
</label>
    <p>总价:{{ count }}</p>
  </div>
</template>

<script lang="ts">
import { watch, defineComponent, ref } from "vue";
export default defineComponent({
  name"SetupWatchMultiple ",

  props: {
    price: {
      typeNumber,
    },
  },

  setup() {
    const num = ref(10);
    const price = ref(5);
    const count = ref(0);
    // ⭐watch
    watch(
      [num, price],
      ([num, price]) => {
        // 注意此处的num和price已经是数字值而不是ref对象.
        // 所以不再需要通过.value访问
        count.value = price * num;
      },
      { immediatetrue }
    );

    return { num, count, price };
  },
});
</script>
Vue3快速入门(二)

props(用toRef/computed包裹)

如果watch监视的是props定义的数据,那么要注意了,watch能监控的数据要求是”ref/computed“函数初始化过的,或者被函数包裹返回的数据。

下图是vue3源码中对watch函数的类型声明:

Vue3快速入门(二)

下面是对被监视数据的类型声明

Vue3快速入门(二)

所以我们如果要监视props返回的数据,可以有3种办法:

1、直接用函数返回prop

watch(
    // ⭐
    [() => props.price, num],

    ([price, num]) => {
        count.value = price * num;
    },
);

2、使用computed包裹一下

watch(
    // ⭐
    [computed(()=>props.price) , num],

    ([price, num]) => {
        count.value = price * num;
    },

    { immediatetrue }
);

3、使用toRef把props数据转成ref数据

watch(
    // ⭐
    [toRef(props,'price') , num],

    ([price, num]) => {
        count.value = price * num;
    },

    { immediatetrue }
);

最终效果如下:

<template>
  <div>
    <label
      >
数量:
      <input
        v-model="num"
        type="number"
        style="padding: 8px; border: 1px solid #ccc"
    />
</label>
    <p>总价:{{ count }}</p>
  </div>
</template>

<script lang="ts">
import { watch, defineComponent, ref, toRef } from "vue";
export default defineComponent({
  name"SetupWatchMultiple ",

  props: {
    price: {
      typeNumber,
      default1,
    },
  },

  setup(props) {
    const num = ref(10);
    const count = ref(0);
    // ⭐watch
    watch(
      // ⭐ 改成函数或者ref/computed函数包裹一下
      [() => props.price, num],
      // 等价于
      // [computed(()=>props.price) , num],
      // 还等价于
      // [toRef(props,'price') , num],

      ([price, num]) => {
        count.value = price * num;
      },

      { immediatetrue }
    );

    return { num, count };
  },
});
</script>

生命周期函数

还记得我们之前介绍过生命周期,当时是以属性的形式去使用的,在setup函数中,他们可以以函数的形式使用。

onMounted

当前组件加载到页面后执行。

<script lang="ts">
// onMounted要单独引入
import { onMounted, defineComponent } from "vue";
export default defineComponent({
  setup() {
  onMounted(()=>{
     alert('onMounted');
    })
  },
});
</script>

onBeforeUnmount

页面组件实例销毁且移出dom之前执行,还记得吗,我们这里一般执行第三发插件的销毁操作。

<template>
  <p ref="target">目标</p>
</template>

<script lang="ts">

import { onMounted,onBeforeUnmount, defineComponent } from "vue";
import AT from 'any-touch';
export default defineComponent({
  setup() {
    let at = null;
    
    // 组件挂载到页面执行的函数.
    onMounted(()=>{
      // 初始化第三方插件
     at = new AT(target.value);
    })
    
    // 组件销毁之前执行的函数.
  onBeforeUnmount(()=>{
      // 销毁第三方插件
     at.destroy();
    })
    
    return {target:ref(null)};
  },
});
</script>

其他钩子

其他钩子在业务开发中不常用,本文并不展开讲了。

有兴趣的小伙伴请阅读官方文档相关章节:https://v3.cn.vuejs.org/guide/composition-api-lifecycle-hooks.html


原文始发于微信公众号(程序员阿晶):Vue3快速入门(二)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/19793.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!