最近学习 Vue3, 这里做个简单的记录,Vue3 在 Vue2 的升级,语法和 API 都发生了变化。下面就一些新增的 API,写一些简单的 demo 进行记录。

创建项目

vue3 创建项目,使用 Vite 脚手架。放弃了原来的 vue-cli

1
2
# 跟着cli的提示一步步创建即可:
npm init vite@latest

我尝试着删除 src 目录,重新新建一个 src 目录,自己写一个 src/main.tssrc/App.vue

App.vue文件引入typescript 文件报错

会发现 ts 检查在 App.vue 中报错。这时因为 ts 的检查中,不认识.vue 文件,需要配置 env.d.ts文件。cmd (Mac)/ctrl (Windows) + 左键点击下面的 "vite/client" 路径

1
/// <reference types="vite/client" />

会跳转到 vite/client 文件,添加:

1
2
3
4
5
declare module "*.vue" {
import { ComponentOptions } from "vue";
const componentOptions: ComponentOptions;
export default componentOptions;
}

即可

Vue3 入口文件一些概念

  • Vite 项目中,index.html 是项目的入口文件,在项目最外层。
  • 加载 index.html 后,Vite 解析 <script type="module" src="xxx">指回的 Javascript
  • Vue3 中是通过createApp函数创建一个应用实例。

Options APIComposition API

Vue2 属于 Option API (配置选项式 API),Vue3 属于 Composition API(组合式 API),我愿意称他为功能性组合 API。

随着 Vue2 里的各个组件越来越复杂,配置式 API 缺点非常明显,所有的值散落在诸如 data(), methods(), computed(), watch()等等方法中,修改一处有可能要修改多处。

比起原来 Options API(例如:data(),methods()),setup有两个特点:

  • 一个 vue 文件可以同时具备 data(), methods() 等 Vue2 的功能,也可以同时具备 setup()写法的 Vue3 功能。
  • setup() 函数中,不能再使用 this。而且setup是最早的生命周期函数。所以 data() 和 methods() 函数中,可以访问 setup() 函数中定义的变量。但 setup() 函数中,不能访问 data() 和 methods() 函数中定义的变量。

setup()

这个是 Vue3 追赶 React 函数式编程的新概念,在 Vue2 里,setup普通的写法,他是执行在生命周期里的 beforeCreate 阶段之前的函数,因此,Vue3 将 setup函数提升到一个新的高度。

1
2
3
4
5
<script>
export default {
setup() {},
};
</script>

语法糖 <script setup lang="ts">

鉴于 setup()的特殊性,Vue3 将其独立为一个语法糖的写法,将其整合进 <script lang="ts" setup> 里,下面是 vue3 App.vue 文件的 setup 改法:

1
2
3
4
5
6
7
8
<template>
<ChildComp />
</template>

<script setup lang="ts">
// 直接导出子组件即可,省略 export default
import ChildComp from "./components/WatchEffectDemo.vue";
</script>

setup插件: VueSetupExtend 增强 script 标签用法

1
npm i vite-plugin-vue-setup-extend -D

vite.config.ts 配置:plugins 中添加 VueSetupExtend(),如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueSetupExtend from "vite-plugin-vue-setup-extend";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), VueSetupExtend()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});

refreactive,将数变为响应式

让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<template>
<div class="car">
<h2>{{ car.brand }} -> {{ car.price }}元</h2>
<button @click="addPrice">修改价格+10万</button>
<button @click="minusPrice">修改价格-1万</button>
<button @click="changeCarByReactive">修改整车by reactive</button>
</div>
<div>
<h2>{{ car2.brand }} -> {{ car2.price }}元</h2>
<button @click="changeCarByRef">修改整车by ref</button>
</div>
</template>

<script lang="ts" setup name="Car12">
import { ref, reactive, toRefs } from "vue";

let car = reactive({ brand: "Toyota", price: 200000 });
// 如果要解构,必须用toRefs套上,否则页面无法响应
let { price } = toRefs(car);
const addPrice = () => (price.value += 100000);

// 如果只修改某一个属性,用.语法修改
const minusPrice = () => (car.price -= 10000);
const changeCarByReactive = () => {
// 如果涉及整个对象修改过,必须用Object.assign
Object.assign(car, { brand: "Audi", price: 300000 });
};

let car2 = ref({ brand: "Benz", price: 800000 });
const changeCarByRef = () => {
// 如果改用ref,必须用value修改,而且.value就是响应式,页面可以实时呈现
car2.value = { brand: "Audi", price: 300000 };
};
</script>
  • ref 可以包裹任意数据类型,作为响应式数据,并实时修改视图的值
  • ref 包裹的数据,如果要更改页面的值,必须用 .value 修改。例如例子中的:car2.value = { brand: "Audi", price: 300000 };
  • reactive 只能用来包裹一个对象,作为响应式数据。
  • reactive 包裹的对象,如果要整体修改,必须用 Object.assign 修改。例如例子中的: Object.assign(car, { brand: "Audi", price: 300000 });
  • 如果遇到解构,必须用toRefs 包裹普通对象,使其变为响应式对象,要不页面上的响应式数据就不会更新。例如例子中的:let { price } = toRefs(car);

computed 计算属性

computed 是一个计算属性,它依赖其他数据,当依赖的数据发生变化时,计算属性会重新计算。这是我们在 Vue2 中经常使用的。

但是 Vue3 中,我们还可以用computed自带的 getter 和 setter 来实现计算属性的获取和修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<template>
<div class="person">
全名:<span>这个是计算属性计算出来的全名:{{ fullName }}</span>
<button @click="changeFullName">点击修改姓名为Li si</button>
</div>
</template>

<script lang="ts" setup name="Person">
import { ref, computed } from "vue";

let firstName = ref("zhang");
let lastName = ref("san");

// 传统vue2,计算属性是只读属性,页面上并不能直接修改:
// const fullName = computed(
// () =>
// `${firstName.value.slice(0, 1).toUpperCase()}${firstName.value.slice(1)} - ${lastName.value}`
// )

// vue3的fullName计算属性,可读可写:用setter,可以直接修改
const fullName = computed({
get() {
return `${firstName.value
.slice(0, 1)
.toUpperCase()}${firstName.value.slice(1)} - ${lastName.value}`;
},
set(val) {
const [f, l] = val.split("-");
firstName.value = f;
lastName.value = l;
},
});

const changeFullName = () => (fullName.value = "li-si");
</script>

watch 监视

watch 是一个监听器,它监听某个数据,当数据发生变化时,执行回调函数。
watch 监听的数据可以是一个响应式数据,也可以是一个普通数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div class="person">
<h2>姓名:{{ person.name }}</h2>
<h2>汽车:{{ person.car.c1 }} {{ person.car.c2 }}</h2>
<button @click="changeName">改名</button>
<button @click="changeCar1">改第一台🚗</button>
<button @click="changeCar2">改第二台🚗</button>
<button @click="changeCars">改所有🚗</button>
</div>
</template>

<script lang="ts" setup>
import { reactive, watch } from "vue";

let person = reactive({
name: "张三",
car: {
c1: "奔驰",
c2: "宝马",
},
});
const changeName = () => (person.name += "~");
const changeCar1 = () => (person.car.c1 = "audi");
const changeCar2 = () => (person.car.c2 = "Toyota");
const changeCars = () => (person.car = { c1: "byd", c2: "yy" });

// reactive监视属性时候,第一个参数必须写为函数返回类型,否则报错
watch(
() => person.name,
(newValue, oldValue) => {
console.log("newValue", newValue);
console.log("oldValue", oldValue);
}
);

// 监视的reactive对象属性也是obj时
// 如果要监视除该
watch(
() => person.car,
(newValue, oldValue) => {
console.log("newValue", newValue);
console.log("oldValue", oldValue);
},
// 加上deep:true,可以监视任意person对象里的细微变化
{ deep: true }
);

// 监视多个属性
watch([() => person.name, () => person.car.c1], (newValue, oldValue) => {
console.log("newValue", newValue);
console.log("oldValue", oldValue);
});
</script>

<style scoped></style>

watch 等价的,有另一个 api watchEffect,它更智能,它接收一个函数,函数里可以写异步代码,当函数执行时,会自动收集依赖,当依赖的数据发生变化时,会重新执行函数。

下面写一个 demo 实现:下面是一个简单的监测水温和水位的例子,当水温达到或超过 60℃,或者水位达到或超过 80cm 时,给服务器发请求。这里的请求用了 console.log,实际应用中,可以发送请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div>
<h2>当水温达到或超过60℃,或者水位达到或超过80cm时,给服务器发请求</h2>
<h2>水温:{{ temp }}</h2>
<h2>水位:{{ height }}</h2>
<button @click="changeTemp">水温+10 ℃</button>
<button @click="changeHeight">水位+10 cm</button>
</div>
</template>

<script lang="ts" setup>
import { ref, watch, watchEffect } from "vue";
let temp = ref(10);
let height = ref(0);

const changeTemp = () => {
temp.value += 10;
};
const changeHeight = () => {
height.value += 10;
};

// watch实现:
watch([temp, height], (value) => {
let [newTemp, newHeight] = value;
if (newTemp >= 60 || newHeight >= 80) {
console.log("给服务器发请求");
}
});

// watchEffect实现:
// wathcEffect可以不用写函数,直接写代码,会自动收集依赖。
watchEffect(() => {
if (temp.value >= 60 || height.value >= 80) {
console.log("给服务器发请求");
}
});
</script>

defineExpose 暴露响应式数据

  1. ref 除了定义一个引用之外,还可以给真实 DOM 元素或组件元素打标记,然后通过 ref 获取到该元素或者组件元素实例。
  2. 和 vue2 不一样,vue3 的引用值 ref 必须用 defineExpose 暴露给父组件,父组件才能获取到他的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div ref="x">hello vue3</div>
<button @click="showRef">get ref DOM</button>
</template>

<script lang="ts" setup>
import { ref, defineExpose } from "vue";

// 和vue2不一样,vue3的引用值ref必须用defineExpose暴露
let a = ref("1");
defineExpose({ a });

// x 必须与上面的 ref 标签名字一致
let x = ref();
const showRef = () => {
console.log(x.value); // <div>hello vue3</div>
};
</script>

ref 打在组件标签上,能打印出组件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<RefDemo ref="aa" />
<button @click="showRef">showRef</button>
</template>

<script setup lang="ts">
import RefDemo from "./components/RefDemo.vue";
import { ref } from "vue";
let aa = ref();
const showRef = () => {
console.log(aa.value); // Proxy(Object) {__v_skip: true}
};
</script>

props, definePropswithDefaults

不废话,直接上 demo,和 React 不同的是,子组件如果要显示 Props,需要用defineProps定义,否则会报错。

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="app">
<PropsDemo a="hehe" :personsList="persons" />
</div>
</template>

<script setup lang="ts">
import PropsDemo from "./components/PropsDemo.vue";

let persons = [
{ id: 1, name: "张三", age: 18 },
{ id: 2, name: "李四", age: 19 },
];
</script>

子组件的写法就有点奇葩,如果定义默认值,还要再引入一个withDefaults,他的定义是比 vue2 啰嗦的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div>{{ a }}</div>
<ul>
<li :key="person.id" v-for="person in personsList">
{{ person.name }}: {{ person.age }}
</li>
</ul>
</template>

<script lang="ts" setup>
import { defineProps, withDefaults } from "vue";
import type { Person } from "@/types";

// 写法1:如果不用ts的普通写法
// const props1 = defineProps(['a', 'personsList'])
// console.log(props1)

// // 写法2:用上ts及withDefaults,当然,withDefaults可以省略不用
const props = withDefaults(
defineProps<{ a: string; personsList: Person[] }>(),
{
a: "xx",
personsList: () => [{ id: "kk", name: "yy", age: 18 }],
}
);

console.log(props); // Proxy(Object) {a: 'hehe', personsList: Array(2)}
</script>

生命周期

Vue2 的生命周期:
创建阶段:beforeCreate created
挂载阶段:beforeMount mounted
更新阶段:beforeUpdate updated
销毀阶段:beforeDestroy destroyed

Vue3 的生命周期:

创建阶段:setup
挂载阶段:onBeforeMount onMounted
更新阶段:onBeforeUpdate onUpdated
销毁阶段:onBeforeUnmount onUnmounted

由于 Vue3 我们把 setup 阶段写在 script 标签里,所以 Vue3 的生命周期比 Vue2 的少了 2 个,分别是beforeCreatecreated

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- vue2的写法: -->
<script>
export default {
created() {
console.log("created");
},
};
</script>

<!-- Vue3的写法: -->
<script setup lang="ts">
onMounted(() => {
console.log("onMounted");
});
</script>

Custom Hooks 模块化封装

这个概念完全是抄袭 React hooks,由于 Vue3 设计成了setup 语法,所以使得函数式编程称为可能,下面是一个 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="person">
<img v-for="(image, idx) in dogImages" :src="image" :key="idx" alt="dog" />
<br />
<button @click="addNewDogImage">添加狗图</button>
</div>
</template>

<script lang="ts" setup>
import useDogImages from "@/hooks/useDogImages";
const { dogImages, addNewDogImage } = useDogImages();
</script>

<style scoped>
img {
height: 150px;
margin-right: 10px;
}
</style>

useDogImages.ts 文件里,可以写下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { ref, reactive } from "vue";

// 添加狗图
export default (url: string = "https://dog.ceo/api/breeds/image/random") => {
// https://dog.ceo/api/breeds/image/random
const defaultDogImage =
"https://images.dog.ceo/breeds/affenpinscher/n02110627_11811.jpg";
const dogImages = reactive([defaultDogImage]);

const addNewDogImage = async () => {
fetch(url)
.then((res) => res.json())
.then((data) => dogImages.push(data.message))
.catch((err) => alert(err));
};

return {
dogImages,
addNewDogImage,
};
};

Pinia 状态管理

pinia 是一个符合直觉的状态管理库,和vuex一样,都是基于vue3的,但是piniavuex更简单易用,而且vuex的作者已经推荐使用pinia了。
pinia 的使用非常简单,只需要在main.ts里引入createPinia,然后app.use(createPinia()),然后就可以在setup函数里使用defineStore定义一个状态了。

1
npm i pinia

createApp

main.ts 引入:

1
2
3
4
5
6
7
8
9
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";

// 下面这个顺序不能乱,必须按照这个顺序,先createPinia(),再app.use(pinia), 再挂载到app的根节点上
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");

defineStore, state, actions

创建一个store/count.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineStore } from "pinia";

const useCountStore = defineStore("count", {
actions: {
addToMaxTwenty(value: number) {
if (this.count <= 20) {
this.count += value;
}
},
},
state() {
return {
count: 0,
by: "X",
};
},
});

export default useCountStore;

在应用中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div class="talk">
<div>
<h1>count:{{ countStore.count }}</h1>
<h2>countStore的其他数据: {{ countStore.by }}</h2>
<!-- v-model.number 可以直接转换为数字类型 -->
<select v-model.number="n">
<option value="1" selected>1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="plus">+</button>
<button @click="minus">-</button>
</div>
</div>
</template>

<script lang="ts" setup>
import { ref, reactive } from "vue";
import useCountStore from "@/store/count";
import useTalkStore from "@/store/talks";

const countStore = useCountStore();
const n = ref(1);
const plus = () => {
// 第一种改法:pinia符合直觉的状态管理,可以直接修改其值,例如:
// countStore.count += n.value

// 第二种改法:$patch
// countStore.$patch({
// count: 1,
// by: 'pinia'
// })

// 第三种改法:actions,这种方法的好处是可以在addToMaxTwenty里添加复杂逻辑
countStore.addToMaxTwenty(n.value);
};
const minus = () => (countStore.count -= n.value);
</script>

defineStore 第二个参数改写为组合式 API

上面的 talkStore 的例子,第二个参数,我们写的是配置式的 API(OptionAPI),但是 pinia 也支持组合式 API,可以改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { defineStore } from "pinia";
import { reactive } from "vue";

type Talk = {
id: number;
content: string;
};
const useTalkStore = defineStore("talks", () => {
const talkList = reactive<Talk[]>(
JSON.parse(localStorage.getItem("talkList") as string) ?? []
);

const assertOneNewTalk = async () => {
try {
const response = await fetch(
"https://api.uomg.com/api/rand.qinghua?format=json"
);
if (!response.ok) throw new Error("Network response was not ok");
const { content } = await response.json();
return talkList.push({ id: talkList.length + 1, content });
} catch (err) {
console.error(err);
}
};

return { talkList, assertOneNewTalk };
});

export default useTalkStore;

storeToRefs

如果我们要省略上面的写法 countStore.count,在 pinia 中,可以使用 storeToRefs 解构,这样我们就可以省略 countStore.count,直接使用 count
这里最好不要使用 vue 自带的 **toRefs**,因为 toRefs 会将所有属性都变成响应式,而 storeToRefs 只会将 state 中的属性变成响应式。

TalkStore 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="talk">
<button @click="addNewTalk">获取一句土味情话</button>
<ul>
<li v-for="talk in talkList" :key="talk.id">{{ talk.content }}</li>
</ul>
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import { storeToRefs } from "pinia";
import useCountStore from "@/store/count";
import useTalkStore from "@/store/talks";

const talkStore = useTalkStore();
// 不能直接解构,const { talkList } = talkStore,这样解构出来的 talkList 不是是响应式对象数据,页面用了不会触发更新
const { talkList } = storeToRefs(talkStore);
const addNewTalk = () => talkStore.assertOneNewTalk();
</script>

store/talks 文件里,可以直接请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { defineStore } from "pinia";

type Talk = {
id: number;
content: string;
};
const useTalkStore = defineStore("talks", {
actions: {
async assertOneNewTalk() {
try {
const response = await fetch(
"https://api.uomg.com/api/rand.qinghua?format=json"
);
if (!response.ok) throw new Error("Network response was not ok");
const { content } = await response.json();
return this.talkList.push({ id: this.talkList.length + 1, content });
} catch (err) {
console.log(err);
}
},
},
state() {
return {
talkList: [] as Talk[],
};
},
});

export default useTalkStore;

getters 处理数据

接着上面的例子,我们在 defineStore 函数中加入:getters, 如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { defineStore } from "pinia";

const useCountStore = defineStore("count", {
actions: {
addToMaxTwenty(value: number) {
if (this.count <= 20) {
this.count += value;
}
},
},
state() {
return {
count: 0,
by: "X",
};
},
// 两种写法均可
getters: {
doubleCount: (state) => state.count * 2,
square(): number {
return this.count ** 2;
},
},
});

export default useCountStore;

这样就可以在 template 里面直接使用 doubleCountsquare 了。

$subscribe store 的订阅

接着上面TalkStore的例子,添加$subscribe,监视数据,并将其存在浏览器,进行简单的数据持久化,如:

1
2
3
talkStore.$subscribe((_, state) => {
localStorage.setItem("talkList", JSON.stringify(state.talkList));
});

再将state里的talkList 换成在 localStorage 里取:当然,必须判断第一次打开页面时,localStorage 里没有数据,所以需要写一下空值判断符:

1
2
3
state: () => ({
talkList: JSON.parse(localStorage.getItem("talkList") as string) ?? [],
});

vue-router 路由

安装

1
npm i vue-router

创建路由

创建 src/router/index.ts,当然,前提还必须创建页面的组件如 Home.vue 等 3 个,这里不详细介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
import News from "@/views/News.vue";
import About from "@/views/About.vue";

const router = createRouter({
// Provide the history routes to use.
// 消灭url的#号
// 但history 路由必须要服务器配合设置
history: createWebHistory(),
routes: [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/news",
name: "News",
component: News,
},
{
path: "/about",
name: "About",
component: About,
},
],
});

export default router;

history 模式 在 Nginx上配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name your-domain.com;

root /path/to/your/webroot;
index index.html;

location / {
# 当Nginx接收到一个请求时,它会首先尝试按照请求的URI提供文件($uri),如果找不到,它会尝试将请求定向到URI对应的目录($uri/),如果还是找不到,那么最终会提供/index.html文件。
try_files $uri $uri/ /index.html;
}
}

main.ts 中,引入路由,并挂载到 app 上:

1
2
3
4
5
6
7
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);
app.use(router);
app.mount("#app");

使用路由

在路由页面可以显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="router-demo">
<h2 class="title">Vue Router</h2>
<div class="navigator">
<RouterLink to="/" active-class="active">首页</RouterLink>
<RouterLink :to="{ name: 'News' }" active-class="active">新闻</RouterLink>
<RouterLink :to="{ path: '/about' }" active-class="active"
>关于</RouterLink
>
</div>
<div class="content">
<RouterView></RouterView>
</div>
</div>
</template>

<script lang="ts" setup>
import { RouterView, RouterLink } from "vue-router";
</script>

<style scoped>
/* a.active 对应上面的 active-class, 其他样式省略 */
.navigator a.active {
background-color: rgb(76, 146, 199);
font-weight: 900;
text-shadow: 0 0 1px black;
}
</style>
  • <RouterView></RouterView> 为各个路由组件的占位符,
  • <RouterLink to="/" active-class="active"/><a/> 标签的二次封装
  • 属性 to 有 2 种写法,可to="/", 也可 :to="{path: '/'}":to="{name: 'Home'}", 这里的 name 对应路由文件 router/index.ts 里定义的 name
  • 路由导航到的页面,挂载到页面的根节点上,切换时则卸载该节点,可以用在具体页面里用 onMountedonUnmounted 来监听,验证这点。

子路由

在上面我们创建的 router/index.ts 文件里,用 news 页面创建二级路由, 在 /news路由下,添加 children 数组,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import NewsDetail from '@/views/NewsDetail.vue'

...
{
path: '/news',
name: 'News',
component: News,
children: [
{
path: 'newsDetail',
name: 'NewsDetail',
component: NewsDetail
}
]
}
...

路由传参

query 传参

这种是放在 url 的后面,如:/news?id=1&title=xxx&content=xxx,比较啰嗦,不太推荐。
改造 views/News.vue 文件,添加 RouterLink 组件,并添加 query 参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div class="news">
<ul>
<li v-for="news in newsList" :key="news.id">
<!-- 1. 利用字符串模板,拼接query参数,太长,比较恶心 -->
<!-- <RouterLink
:to="`/news/newsDetail?id=${news.id}&title=${news.title}&content=${news.content}`"
>{{ news.title }}</RouterLink
> -->

<!-- 2. 以对象拼接query参数 -->
<RouterLink
:to="{
path: '/news/newsDetail',
query: {
id: news.id,
title: news.title,
content: news.content
}
}"
>{{ news.title }}
</RouterLink>
</li>
</ul>
<div class="news-content">
<RouterView></RouterView>
</div>
</div>
</template>

<script lang="ts" setup>
import { reactive } from "vue";
import { RouterView, RouterLink } from "vue-router";
const newsList = reactive([
{ id: 1, title: "1111111", content: "aaaaaa" },
{ id: 2, title: "2222222", content: "bbbbbb" },
{ id: 3, title: "3333333", content: "cccccc" },
]);
</script>

views/NewsDetail.vue 文件里,添加 props 接收传参,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<div>编号:{{ query.id }}</div>
<div>标题:{{ query.title }}</div>
<div>内容:{{ query.content }}</div>
</div>
</template>

<script lang="ts" setup>
import { toRefs } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const { query } = toRefs(route);
</script>

params 传参

这种传参是放在 url 的后面,如:/news/newsDetail/1,写起来比 query 传参简洁些

先改造 router/index.ts 文件,添加 params 参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
path: '/news',
name: 'News',
component: News,
children: [
{
path: 'newsDetail/:id/:title/:content',
name: 'NewsDetail',
component: NewsDetail
}
]
},

views/News.vue 文件,改造如下,值得注意的是,写法 2 对象里不能用 path,必须写路由里的 name 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<template>
<div class="news">
<ul>
<li v-for="news in newsList" :key="news.id">
<!-- parmas传参写法1 -->
<!-- <RouterLink :to="`/news/newsDetail/${news.id}/${news.title}/${news.content}`">{{
news.title
}}</RouterLink> -->

<!-- parmas传参写法2 -->
<RouterLink
:to="{
name: 'NewsDetail',
params: {
id: news.id,
title: news.title,
content: news.content
}
}"
>{{ news.title }}
</RouterLink>
</li>
</ul>
<div class="news-content">
<RouterView></RouterView>
</div>
</div>
</template>

<script lang="ts" setup>
import { reactive } from "vue";
import { RouterView, RouterLink } from "vue-router";
const newsList = reactive([
{ id: 1, title: "1111111", content: "aaaaaa" },
{ id: 2, title: "2222222", content: "bbbbbb" },
{ id: 3, title: "3333333", content: "cccccc" },
]);
</script>

NewsDetail.vue 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div>
<div>编号:{{ params.id }}</div>
<div>标题:{{ params.title }}</div>
<div>内容:{{ params.content }}</div>
</div>
</template>

<script lang="ts" setup>
import { toRefs } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const { params } = toRefs(route);
</script>

路由props参数

上面提到的params传参,都可以通过 props 参数来接收,最推荐这种写法,改写如下:

router/index.ts 文件里,添加 props 参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
path: '/news',
name: 'News',
component: News,
children: [
{
path: 'newsDetail/:id/:title/:content',
name: 'NewsDetail',
component: NewsDetail,
props: true,

// props可以写成回调函数的写法,但没必要
// props: ({ params: { id, title, content } }) => ({
// id
// title,
// content
// })
}
]
}

views/News.vue 文件 和上面提到的一样,无需改动,views/NewsDetail.vue 改造如下:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<div>编号:{{ id }}</div>
<div>标题:{{ title }}</div>
<div>内容:{{ content }}</div>
</div>
</template>

<script lang="ts" setup>
defineProps(["id", "title", "content"]);
</script>

replace

replace 是一个<RouterLink>组件的属性,加上则表示不缓存该页面,不能点击浏览器上的后退按钮,如:

1
<RouterLink replace to="/" active-class="active">首页</RouterLink>

useRouter 编程式路由导航

上面我们提到的例子都是<RouterLink>组件,实际应用中,还有一种编程式路由导航,使用频率远大于<RouterLink>组件,例如我们用上面的例子,在首页有一个 button,点击可以跳转到 news 新闻页面,views/Home.vue文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>home page</div>
<button @click="jumpToNews">jump to news page</button>
</template>

<script lang="ts" setup>
import { useRouter } from "vue-router";
const navigateTo = useRouter();
const jumpToNews = () => {
// 用push:
// navigateTo.push('/news')
// 也可这种写法:
navigateTo.push({
path: "/news",
});

// 用replace:不能后退
// navigateTo.replace('/news')
};
</script>

redirect 重定向

这个太简单,不赘述了,直接上代码router/index.ts

1
2
3
4
{
path: "/",
redirect: "/home",
}

组件间通信 ICC inner-component communication

父子互传

definedProps 和回调函数

父子组件间通信,demo 如下:

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="parent">
<h3>parent</h3>
<h4>parent's house:{{ house }}</h4>
<h4>toy fr child:{{ toy }}</h4>
<Child :house="house" :sendToyToParent="getToyFromChild" />
</div>
</template>

<script lang="ts" setup>
import Child from "./ChildComp.vue";
import { ref } from "vue";

const house = ref("🏠");

// 回调函数:在Child上定义的是sendToyToParent
const toy = ref("");
const getToyFromChild = (value: string) => {
console.log(value);
toy.value = value;
};
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="app">
house fr parent:{{ house }}
<div>my toy: {{ toy }}</div>
<button @click="sendToyToParent(toy)">send to parent</button>
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const toy = ref("🐻");
// 第一个参数是父组件传过来的数据,第二个是父组件传过来的函数
defineProps(["house", "sendToyToParent"]);
</script>

v-model 双绑 -> 组件封装

说实话,这个属性在项目中是不常用的特性,项目中我们多用第三方组件库,

而 vue2 的v-model必须写在原生的 input 框上,

但 vue3 中的 v-model,可以直接写在组件标签上,有了这个特性,可以使其拥有更多的用法

首先,我们先看一下 v-model 在 vue2 中的实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>{{ inputValue }}</div>
<!-- v-model 用在HTML标签上 -->
<input v-model="inputValue" type="text" />
<!-- 原理: -->
<input
:value="inputValue"
type="text"
@input="inputValue = ($event.target as HTMLInputElement).value"
/>
</template>

<script lang="ts" setup>
import { ref } from "vue";
const inputValue = ref("hello");
</script>

原始的 v-model 是一个语法糖,实际上,是将 input 框的 :value@input,利用这个特性,我们可以实现一个多 input 的组件,实现和 vue2 的 v-model 一样的效果。

被封装的多 input 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<input
type="text"
:value="uName"
@input="emit('update:uName', ($event.target as HTMLInputElement).value)"
/>

<input
type="password"
:value="pwd"
@input="emit('update:pwd', ($event.target as HTMLInputElement).value)"
/>
</template>

<script lang="ts" setup>
defineProps(["uName", "pwd"]);
const emit = defineEmits(["update:uName", "update:pwd"]);
</script>

<style scoped>
/* 特别牛叉的样式: */
input {
background-color: #26b9b1;
}
</style>

在页面里使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h4>{{ userName }}</h4>
<h4>{{ userPwd }}</h4>
<Inputs v-model:uName="userName" v-model:pwd="userPwd" />
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Inputs from "./FormInputs.vue";

const userName = ref("hello");
const userPwd = ref("123456");
</script>

通过以上的例子,可以看出以下特征:

  • 被封装的组件中,:value@input 事件,实现了使用中 <Inputs v-model:uName="userName" v-model:pwd="userPwd" />v-model 效果。
  • v-model:uName="userName" 可以取别名uName,我们在封装的组件里提取时,也可以使用其别名

slot

相当于 react 的 children,自定义组件双标签中包的那一块内容

匿名插槽:数据 父->子

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="parent">
<h3>parent Comp</h3>
<div class="content">
<Category title="热门游戏列表">
<ul>
<li v-for="game in ganmes" :key="game.id">{{ game.name }}</li>
</ul>
</Category>
</div>
</div>
</template>

<script lang="ts" setup>
import Category from "./CategoryBoard.vue";
import { reactive } from "vue";

const ganmes = reactive([
{ id: 1, name: "英雄联盟" },
{ id: 2, name: "王者荣耀" },
{ id: 3, name: "绝地求生" },
{ id: 4, name: "原神" },
]);
</script>

子组件:相当于用 slot 进行占位 html 的内容:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="category">
<h2>{{ title }}</h2>
<!-- slot 相当于react 的 children -->
<slot>默认内容</slot>
</div>
</template>

<script lang="ts" setup>
defineProps(["title"]);
</script>
具名插槽:数据 父->子

父组件:其中 #s1 #s2 是插槽的名称,可以自定义,#s1 号是v-slot:s1的缩写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<div class="parent">
<h3>parent Comp</h3>
<div class="content">
<Category title="热门游戏列表">
<template #s1>
<h2>热门游戏列表</h2>
</template>
<template #s2>
<div class="games">
<div v-for="game in ganmes" :key="game.id">{{ game.name }}</div>
</div>
</template>
</Category>
</div>
</div>
</template>

<script lang="ts" setup>
import Category from "./CategoryBoard.vue";
import { reactive } from "vue";

const ganmes = reactive([
{ id: 1, name: "英雄联盟" },
{ id: 2, name: "王者荣耀" },
{ id: 3, name: "绝地求生" },
{ id: 4, name: "原神" },
]);
</script>

子组件:可以看到,slot 分为 s1 和 s2,分别对应子组件中的上下的两个插槽

1
2
3
4
5
6
<template>
<div class="category">
<slot name="s1">默认内容1</slot>
<slot name="s2">默认内容2</slot>
</div>
</template>
作用域插槽:数据 子->父

简单来说,这个是要解决数据放在子组件处,将子组件数据传递给父组件而设计的插槽。

父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="game">
<ScopedSlotChild>
<template v-slot:game-slot="params">
<ol>
<li v-for="game in params.games" :key="game.id">{{ game.name }}</li>
</ol>
</template>
</ScopedSlotChild>
</div>
</template>

<script lang="ts" setup>
import ScopedSlotChild from "./ScopedSlotChild.vue";
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="game">
<h2>游戏列表</h2>
<slot name="game-slot" :games="games"></slot>
</div>
</template>

<script lang="ts" setup>
import { reactive } from "vue";

const games = reactive([
{ id: 1, name: "Dota 2" },
{ id: 2, name: "Counter-Strike: Global Offensive" },
{ id: 3, name: "League of Legends" },
]);
</script>

以上有几个注意的点:

  • slot 的 name 属性 name="game-slot",可以省略,当 slot 没有 name 属性,那父组件也不用起别名
  • slot 的 name 属性如果出现,那么在父组件的别名,例如:v-slot:game-slot="params":game 必须和 slot 的 name 属性一致
  • 当 slot 的 name 属性值为 default 时,父组件的别名也可以省略

emit 事件

这种方案其实我比较反感,因为上面的回调函数完全可以取代他,但既然框架提供了,那也得介绍一下

接着上面的例子,简单来说就是在子组件中

  • 将点击事件加上 emit api
  • 根据 vue2 的文档,将命名由上面的驼峰法,改为 kebab-case 命名法则,即用 - 连接
1
2
3
4
5
6
7
<button @click="emit('send-toy-to-parent', toy)">send to parent</button>

<script>
// ...

const emit = defineEmits(["send-toy-to-parent"]);
</script>

父组件中,接收子组件的点击事件,并做相应处理

  • 将原来的 :click 改为 vue2 的写法: @send-toy-to-parent,如下:
1
<Child :house="house" @send-toy-to-parent="getToyFromChild" />

$ref -> 父子, $parent -> 子父

先上代码,父组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="parent">
<h3>parent comp</h3>
<h4>house: {{ house }}</h4>
<button @click="getAllChildrenBooks($refs)">+ Children 3 books</button>
<Child1Comp ref="child1" />
<Child2Comp ref="child2" />
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Child1Comp from "./Child1Comp.vue";
import Child2Comp from "./Child2Comp.vue";

const house = ref(4);

const getAllChildrenBooks = (refs: { [x: string]: any }) => {
for (let key in refs) {
refs[key].books += 3;
}
};

defineExpose({ house });
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="child">
<h3>Child 1 Comp</h3>
<h4>books: {{ books }}</h4>
<button @click="minusHouse($parent)">take one house</button>
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const books = ref(3);
defineExpose({ books });

const minusHouse = (parent: any) => {
if (parent.house <= 0) return;
parent.house--;
};
</script>
  • 可以看出这个属性得配合 defineExpose() 使用,且必须是父子组件
  • 在各自组件里,通过回调函数 $parent 获取父组件,通过 $refs 获取子组件
  • 这种写法,允许在父组件获取多个子组件的变量值数据

隔代\跨代传值

$attr

ParentComp.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="parent">
<h1>Parent Comp</h1>
<h2>a: {{ a }}</h2>
<h2>b: {{ b }}</h2>
<ChildComp :a="a" :b="b" v-bind="{ x: 100 }" :updateA="updateA" />
</div>
</template>

<script lang="ts" setup>
import ChildComp from "./ChildComp.vue";
import { ref } from "vue";

const a = ref(1);
const b = ref(2);

const updateA = (val: number) => {
a.value += val;
};
</script>

ChildComp.vue中间的组件,仅写一个属性就够了 v-bind="$attrs"

1
2
3
4
5
6
7
8
9
10
<template>
<div class="child">
<h1>Child Comp</h1>
<GrandChildComp v-bind="$attrs" />
</div>
</template>

<script lang="ts" setup>
import GrandChildComp from "./GrandChildComp.vue";
</script>

GrandChildComp.vue最底层的组件直接接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="grandChild">
<h1>Grand Child Comp</h1>
<h2>props from Parent:</h2>
<h3>{{ a }}</h3>
<h3>{{ b }}</h3>
<h3>{{ x }}</h3>
<button @click="updateA(3)">cb to parent updateA</button>
</div>
</template>

<script lang="ts" setup>
defineProps(["a", "b", "x", "updateA"]);
</script>

从上面的代码可以看出,中间的层级如果只写 v-bind="$attrs" 就可以无限层级的传参,而从最底层的子组件开始,如果要回传到最顶层,也只需写回调函数即可

多层级传参

provide + inject

这个比起上一个更简单,上一个中间组件还需要写一个 v-bind="$attrs"属性,而这种方法中间组件不用添加任何属性

祖先组件用 provide 注入数据,后代组件用 inject 获取数据

祖先组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<template>
<div class="parent">
<h1>Parent Comp</h1>
<h2>house: {{ house }} 万元</h2>
<h2>car: {{ car.price }} 万元</h2>
<ChildComp />
</div>
</template>

<script lang="ts" setup>
import ChildComp from "./ChildComp.vue";
import { ref, reactive, provide } from "vue";

const house = ref(800);
const car = reactive({
brand: "Benz",
price: 80,
});

const updateHousePrice = (value: number) => {
house.value -= value;
};

provide("houseContext", { house, updateHousePrice });
provide("car", car);
</script>

最底层的孙子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div class="grandgrandChild">
<h1>Grand Grand Child Comp</h1>
<h2>house & car from parent:</h2>
<h3>house: {{ house }} 万元</h3>
<h3>car: 一辆{{ y.brand }}车,价值{{ y.price }} 万元</h3>
<button @click="updateHousePrice(1)">修改house价值</button>
</div>
</template>

<script lang="ts" setup>
import { inject } from "vue";

// 直接解构出回调函数updateHousePrice
const { house, updateHousePrice } = inject("houseContext", {
house: 0,
updateHousePrice: (num: number) => {},
});
const y = inject("car", { brand: "BMW", price: 60 });
</script>

从上面的代码可以看出:

  • provide 的第一个参数是定义所传的值的名字,第二个参数可以是单一值,也可以是复杂的对象
  • inject 的第一个参数是定义所传的值的名字,第二个参数是默认值,如果传值失败,则使用默认值
  • provideinject 是 vue3 的新特性,在 vue2 中,可以通过 $parent$children 来实现

任意组件互传

mitt

这是一个独立压缩后只有几百字节的库 mitt,node 环境下均可用,不仅仅局限于 vue

1
npm install mitt

创建 utils/emitter.ts 文件

1
2
3
4
5
6
7
8
import mitt, { type Emitter } from "mitt";

type Events = {
[key: string]: string;
};
const emitter: Emitter<Events> = mitt();

export default emitter;

组件 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div class="comp1">
<h2>组件1</h2>
<h3>我的玩具:{{ toyCar }}</h3>
<button @click="emitter.emit('send-to-comp2', toyCar)">发送给组件2</button>
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import emitter from "@/utils/emitter";

const toyCar = ref("🚙");
</script>

组件 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="comp2">
<div>my toy: {{ toyCarFrComp1 }}</div>
</div>
</template>

<script lang="ts" setup>
import { ref, onUnmounted } from "vue";
import emitter from "@/utils/emitter";

const toyCarFrComp1 = ref("");
emitter.on("send-to-comp2", (value: string) => {
toyCarFrComp1.value = value;
});

onUnmounted(() => {
emitter.off("send-to-comp2");
});
</script>
  • 发送数据方,可以看到上面的 emitter 里调用了 .emit() 方法,.emit() 方法接收两个参数,第一个参数是事件名称,第二个参数是事件携带的数据。
  • 接受数据方,可以看到上面的 emitter 里调用了 .on() 方法,.on() 方法接收两个参数,第一个参数是事件名称,第二个参数是事件监听函数。赋值给页面对应的变量名
  • onUnmounted 的作用是在组件卸载时,也卸载掉监听事件,避免内存留存

Pinia

这种在上面单独拎出来的讲了,这里就不多说了,直接看上面的例子

customRef 自定义响应式

这是一个常用的 api,例如实现防抖,我们用普通 ref 时加上双向数据绑定时,input 框输入实时变化都会实时渲染,如果页面信息过多,那么会影响性能,所以需要用到 customRef实现防抖功能:

我们把他写为一个钩子函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { customRef } from "vue";

const useDelayInput = (initVal: string = "", delay: number = 500) => {
const msg = customRef((track, trigger) => {
return {
// msg 被读取时调用:
get() {
track(); //
return initVal;
},
// msg 被修改时调用:
set(value) {
clearTimeout(delay);
delay = setTimeout(() => {
initVal = value;
trigger();
}, delay);
},
};
});
return msg;
};

export default useDelayInput;

使用:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="app">
<h2>{{ msg }}</h2>
<input type="text" v-model="msg" />
</div>
</template>

<script lang="ts" setup>
import useDelayInput from "../hooks/useDelayInput";
const msg = useDelayInput("customRef", 800);
</script>

从钩子函数可以出:

  • customRef 里是一个函数,返回一个对象,对象里有 getset 方法,get 方法是读取数据,set 方法是修改数据
  • tracktrigger 是两个函数,track 是读取数据时调用,trigger 是修改数据时调用
  • track 的作用是告诉 vue 追踪依赖,trigger 是告诉 vue 触发依赖

shallowRefshallowReactive

这 2 个 api 旨在对付那些比较大的 Object,只监听第一层,层次比较深的数据不会被监听到

shallowRef 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="ts" setup>
import { shallowRef, shallowReactive } from "vue";

// 当修改以下数据时:
const toy = shallowRef({
toy: "🚗",
});
const change = () => {
// ❌ 无效修改:shallowRef 只监听第一层,只会监听到 toy.value
// toy.value.toy = "🚙";

// ✅ 修改成功
toy.value = {
toy: "🚙",
};
};
</script>

shallowRef 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script lang="ts" setup>
import { shallowReactive } from "vue";
const nations = shallowRef({
China: "CN",
});
const changenations = () => {
// ❌无效修改:
// nations.value.China = '中国'

// ✅ 修改成功
nations.value = {
China: "中国",
};
};
</script>

shallowReactive 举例:

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts" setup>
const toys = shallowReactive({
car: "🚗",
animals: { dog: "🐭", cat: "🐱" },
});

// ✅ 修改成功: 位于对象第一层:
const changeToyCar = () => (toys.car = "🚙");
// ❌ 无效修改:位于对象更深的层次:
const changeAnimals = () => (toys.animals.dog = "🐶");
</script>

但当我把上面 const changeToyCar = () => (toys.car = "🚙"); 函数改为:

1
2
3
4
5
6
7
8
<script lang="ts" setup>
// ✅ 修改成功:
// ❗️但不推荐,因为对象会触发两次修改:
const changeToyCar = () => {
toys.car = "🚙";
toys.animals.dog = "🐶";
};
</script>

确两个都成功了,toys.animals.dog,尽管这个属性不是响应式的,但因为之前对 toys.car 的修改已经触发了更新,所以任何依赖于 toys 的视图都会重新渲染,这时你会看到 toys.animals.dog 的新值。所以不推荐这种写法,他与shallowRef的原意背道而驰,直接用 ref即可。

readOnlyshallowReadOnly

对象整体只读和浅层只读,只针对于 reactive 包裹的对象属性,看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div>
<p>Deep Readonly Car: {{ deepReadonlyCar }}</p>
<p>Shallow Readonly Car: {{ shallowReadonlyCar }}</p>
<p>
Nested Property in Shallow Readonly: {{ shallowReadonlyCar.details.color
}}
</p>
<button @click="tryToModify">Try to Modify</button>
</div>
</template>

<script lang="ts" setup>
import { readonly, shallowReadonly, reactive } from "vue";

// 创建一个响应式对象
const car = reactive({
model: "Tesla Model 3",
details: {
color: "Red",
year: 2023,
},
});

// 使用 readonly 创建一个深层只读的响应式对象
const deepReadonlyCar = readonly(car);

// 使用 shallowReadonly 创建一个浅层只读的响应式对象
const shallowReadonlyCar = shallowReadonly(car);

// 尝试修改对象的函数
const tryToModify = () => {
// ❌ 这个对象只读,所以控制台会发出警告 warn 不能修改
deepReadonlyCar.model = "Tesla Model S";

// ❌ 浅层只读对象,依然不能修改
shallowReadonlyCar.model = "Tesla Model X"; // 这将不会生效,并且控制台会给出警告

// ✅ 成功,shallowReadonlyCar 的嵌套对象属性,因为 shallowReadonly 是浅层的
shallowReadonlyCar.details.color = "Blue"; // 这将生效,但修改不会触发响应式更新
};
</script>

toRawmarkRaw

toRaw : 这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特辣方法。不建议保存对原始对象的持久引用,请还慎使用。
toRaw 何时使用?—-在露要将响应式对象传递给非 Vue 的库或外部系统时,使用 toRaw 可以确保它们收到的是普通对象

markRaw 标记一个对象,使其永远不会变成响应式的

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<p>Raw Value: {{ rawValue.foo }}</p>
<p>Marked Raw Value: {{ markedRawValue.foo }}</p>
<button @click="modifyToRaw">Modify to Raw</button>
<button @click="modifyMarkRaw">Modify Marked Raw</button>
</div>
</template>

<script setup>
import { reactive, toRaw, markRaw } from "vue";

const reactiveValue = reactive({ foo: "Reactive Foo" });

// 使用 toRaw 获取响应式对象的原始对象
const rawValue = toRaw(reactiveValue);

const originalObject = { foo: "Marked Raw Foo" };
const markedRawValue = markRaw(originalObject);

const modifyToRaw = () => {
// 修改原始对象的属性,不会触发视图更新,因为它不是响应式的
rawValue.foo = "Modified Raw Foo";
};

const modifyMarkRaw = () => {
// 修改被标记为原始的对象,同样不会触发视图更新
markedRawValue.foo = "Modified Marked Raw Foo";
};
</script>

Teleport

这个类似 React 的 createPortal 功能。
我们看看 React 官网的示例代码:

1
2
3
4
<div>
<SomeComponent />
{createPortal(children, domNode, key?)}
</div>

实际使用中,我们多把弹框从底层传送到 <body> 上,而不是某层的子组件上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<button @click="showModal = true">pop out modal</button>

<teleport to="body">
<div v-if="showModal" class="modal">
<h2>title</h2>
<p>this is a modal create by teleport</p>
<button @click="showModal = false">close</button>
</div>
</teleport>
</div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
const showModal = ref(false);
</script>

suspence 异步组件

参考了 React 18 的 suspence 组件,suspence也是 Vue3 的新特性,用于实现组件的懒加载。

使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<h1>Vue 3 Suspense Example</h1>
<Suspense>
<template #default>
<!-- <AsyncComponent /> -->
<FetchComp />
</template>
<template #fallback>
<h1>Loading...</h1>
</template>
</Suspense>
<FetchComp />
</div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, Suspense } from "vue";
import FetchComp from "./FetchComp.vue";

// import()定义一个异步组件
const AsyncComponent = defineAsyncComponent(() => import("./AsyncComp.vue"));
</script>

FetchComp.vue

1
2
3
4
5
6
7
8
9
10
11
<template>
<h2>fetch comp</h2>
<div class="chlid">{{ content }}</div>
</template>

<script lang="ts" setup>
const request = await fetch(
"https://api.uomg.com/api/rand.qinghua?format=json"
);
const { content } = await request.json();
</script>

全局 api

Vue3 在 main.ts 中,可以用全局 api 注册全局组件,

  1. app.component(name, component) 注册全局组件
  2. app.directive(name, directive) 注册全局指令
  3. app.provide(key, value) 注册全局 provide/inject
  4. app.config.globalProperties.$foo = 'bar' 注册全局属性
  5. app.unmount(element) 卸载应用
  6. app.use(plugin) 安装插件

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
import router from "./router";
import Hello from "./GlobalHello.vue";

const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.mount("#app");

// 定义全局组件:
app.component("globalHello", Hello);

// 定义全局变量:
app.config.globalProperties.globalVariable = 999;
// 官方建议 type 声明:
declare module "vue" {
interface ComponentCustomProperties {
globalVariable: number;
}
}

// 自定义指令:
app.directive("focus", (ele) => {
ele.style.color = "red";
ele.style.backgoundColor = "green";
});

在任意一个 src 下的组件中使用,且不需要 import

1
2
3
4
5
6
7
<template>
<GlobalHello />
<div>{{ globalNumber }}</div>
<input v-focus />
</template>

<script lang="ts" setup></script>

效果如下图:

App.vue文件引入typescript 文件报错

非兼容性改变

可以参考官方文档

重点关注这些比较常用的:

  • 过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from
  • keyCode 作为 v-on 修饰符的支持
  • v-model 指令在组件上的使用已经被重新设计,替换掉了 y-bind.sync
  • v-ifv-for 在同一个元素身上使用时的优先级发生了变化。v-if 优先级 > v-for 优先级
  • 移除了 $on$off$once 实例方法。
  • 移除了过滤器 filter
  • 移除了 $children 实例 propert