Vue3 基础
1.Vue3 大体情况
1) 了解相关信息
- 2 年多开发, 100+位贡献者, 2600+次提交, 600+次 PR
- Vue3 支持 vue2 的大多数特性
- 更好的支持 Typescript
2) 性能提升
- 打包大小减少 41%
- 通过摇树优化核心库体积
- 初次渲染快 55%, 更新渲染快 133%
- 虚拟 DOM 重写
- 优化 slots 的生成
- 静态树提升
- 静态属性提升
- 基于 Proxy 的响应式系统
- 内存减少 54%
- 重写虚拟 DOM 的实现和 Tree-Shaking
- 更容易维护
- TypeScript + 模块化
- 更加友好
- 跨平台:编译器核心和运行时核心与平台无关,使得 Vue 更容易与任何平台(Web、Android、iOS)一起使用
- 更容易使用
- 改进的 TypeScript 支持,编辑器能提供强有力的类型检查和错误及警告
- 更好的调试支持
- 独立的响应化模块
- Composition API
3) Composition(组合) API
-
setup
- setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口
- setup 函数会在 beforeCreate 之前执行,所以没有 this 对象
- 第一个形参 props 用来接收 props 数据,第二个形参 context 用来定义上下文
-
ref 和 reactive
- reactive() 函数接收一个普通对象,返回一个响应式的数据对象
- ref() 函数用来根据给定的值创建一个响应式的数据对象,ref() 函数调用的返回值是一个对象,这个对象上只包含一个 value 属性
-
isRef
- isRef() 用来判断某个值是否为 ref() 创建出来的对象
-
toRefs
-
toRefs() 函数可以将 reactive() 创建出来的响应式对象,转换为普通的对象,只不过,这个对象上的每个属性节点,都是 ref() 类型的响应式数据
-
computed
- 创建只读的计算属性
- 创建可读可写的计算属性
-
watch
- 可以实现普通的监控处理
- 监视指定的数据源,包括: 监视 reactive 类型的数据源,监视 ref 类型的数据源
- 监视多个数据源
- 可以清除监视
-
新的生命周期函数
Vue3 | Vue2 |
---|---|
use setup() | |
use setup() | |
onBeforeMount | beforeMount |
onMounted | mounted |
onBeforeUpdate | beforeUpdate |
onUpdated | updated |
onBeforeUnmount | beforeDestroy |
onUnmounted | destroyed |
onErrorCaptured | errorCaptured |
onRenderTracked | renderTracked |
onRenderTriggered | renderTriggered |
- 自定义 hooks 函数
4) 其它新增特性
- Teleport - 瞬移组件的位置
- Suspense - 异步加载组件的 loading 界面
- 全局 API 的修改
2.创建 vue3 项目
1) 使用 vue-cli 创建(目前还是建议用 Vue-CLI 创建 Vue3 项目)
文档: https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create
## 安装或者升级
npm install -g @vue/cli
## 保证 vue cli 版本在 4.5.0 以上
vue --version
## 创建项目
vue create my-project
然后的步骤
- Please pick a preset - 选择 Manually select features
- Check the features needed for your project - 多选择上 TypeScript && Router && Vuex,特别注意点空格是选择,点回车是下一步
- Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
- Use class-style component syntax - 直接回车
- Use Babel alongside TypeScript - 直接回车
- Pick a linter / formatter config - 直接回车
- Use history mode for router? - 直接回车
- Pick a linter / formatter config - 直接回车
- Pick additional lint features - 直接回车
- Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
- Save this as a preset for future projects? - 直接回车
::: tip Vue3 初始项目直观差异
Vue3 与 Vue2 项目细节差异
:::
(1) 入口文件
Vue3
main.ts
createApp(App).use(store).use(router).mount('#app');
Vue2
main.js
new Vue({
router,
store,
render: (h) => h(App),
}).$mount('#app');
(2) 路由操作
Vue3
router/index.ts(函数化单个拆分引入,方便摇树去除代码,减少项目大小提高性能)
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
mode 切换成了函数化声明
const router = createRouter({
history: createWebHashHistory(),
routes,
});
Vue2
router/index.js(整个模块引入,导致项目大小增加,增大损耗)
import VueRouter from 'vue-router';
mode 是字符串设置
const router = new VueRouter({
routes,
});
(3) 状态管理
Vue3
store/index.ts
import { createStore } from 'vuex';
export default createStore({});
Vue2
store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({});
(4) 页面引入组件
Vue3
views/home.vue
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
@Options({
components: {
HelloWorld
}
})
export default class Home extends Vue {}
</script>
上述代码可以修改成以下代码内容,定义组件的方式也可以利用 defineComponent 来实现
<script lang="ts">
import { defineComponent } from 'vue'
import HelloWorld from "@/components/HelloWorld.vue";
export default defineComponent({
name:'Home',
components:{
HelloWorld
}
})
</script>
Vue2
views/home.vue
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default {
name: 'Home',
components: {
HelloWorld
}
}
</script>
(5) 组件的定义
Vue3
components/helloworld.vue
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string;
}
</script>
上述代码可以修改成以下代码内容,定义组件的方式也可以利用 defineComponent 来实现
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name:"HelloWorld",
props:{
msg:String
}
})
</script>
Vue2
components/helloworld.vue
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
2) 使用 vite 创建(周边支持并不完善)
文档: https://v3.cn.vuejs.org/guide/installation.html
Vite 是一个由原生 ESM 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev
3.Composition API 使用(重要)
compostion API VS Option API
文档:
https://v3.cn.vuejs.org/api/basic-reactivity.html
https://v3.cn.vuejs.org/api/composition-api.html
https://composition-api.vuejs.org/zh/
1) setup
setup
函数是一个新的组件选项。作为在组件内使用 Composition API 的入口点。
创建组件实例,然后初始化 props
,紧接着就调用setup
函数。从生命周期钩子的视角来看,它会在 beforeCreate
钩子之前被调用
入参:
{Data} props
{SetupContext} context
<script lang="ts">
export default {
setup(props,context){
console.log(props,context)
}
}
</script>
2) ref
接受一个参数值并返回一个响应式且可改变的 ref 对象
ref 对象拥有一个指向内部值的单一属性 .value
如果传入 ref 的是一个对象,将调用 reactive
方法进行深层响应转换。
当 ref 作为渲染上下文的属性返回(即在setup()
返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value
(1) 确认 this 对象开始的位置
<script lang="ts">
export default {
beforeCreate(){
console.log('beforeCreate()',this) // beforeCreate在setup之后执行,具有this对象
},
setup(props,context){
console.log('setup()',this) // setup在beforeCreate之前执行,没有this对象
}
}
</script>
(2) 声明 ref 对象
::: tip Vue3 与 Vue2 的 ref 差异
与 Vue2 的 ref 对象指向不同(找 DOM 以及找 Component 组件),Vue3 中的 ref 是创建包含响应式数据的引用对象
:::
<script lang="ts">
import { ref } from "vue"
export default {
beforeCreate(){
console.log('beforeCreate()',this)
},
setup(props,context){
console.log('setup()',this)
// 包含响应式数据的引用对象
const count = ref(0)
// count是一个引用对象, 内部包含存储数据的value属性
console.log(count,count.value)
}
}
</script>
::: tip 对象需返回才能使用
如果目前 count 对象没有进行 return 返回处理,在模板页中插值显示 count 是无法显示的
:::
<script lang="ts">
import { ref } from "vue"
export default {
beforeCreate(){
console.log('beforeCreate()',this)
},
setup(props,context){
console.log('setup()',this)
// 包含响应式数据的引用对象
const count = ref(0)
// count是一个引用对象, 内部包含存储数据的value属性
console.log(count,count.value)
return {
count
}
}
}
</script>
(3) 更新响应式数据的函数
<template>
<div class="about">
<h2>{{ count }}</h2>
<hr />
<button @click="increment">加1</button>
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
export default {
beforeCreate() {
console.log('beforeCreate()', this);
},
// 在beforeCreate()之前执行, 不能通过this访问组件对象
setup() {
console.log('setup()', this);
// 包含响应式数据的引用对象
const count = ref(0); // count是一个引用对象, 内部包含存储数据的value属性
console.log(count, count.value);
// 更新响应式数据的函数
const increment = () => {
count.value++; // 是对value值进行操作处理,不是对引用对象进行处理
};
return {
// 对象中的属性和方法, 模板可以直接访问
count,
increment,
};
},
};
</script>
3) computed
使用 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
或者,它可以使用具有 get
和 set
函数的对象来创建可写的 ref 对象。
(1) 属性计算需要从 vue 中引入 computed 方法,默认操作是 getter
<template>
<div class="about">
<h2>count: {{ count }}</h2>
<h2>double: {{ double }}</h2>
<hr />
<button @click="increment">加1</button>
</div>
</template>
<script lang="ts">
import { computed, ref } from 'vue';
export default {
setup() {
const count = ref(0);
// 计算属性本质上也是一个ref对象
const double = computed(() => {
console.log('double computed', count.value);
return count.value * 2;
});
const increment = () => {
count.value++;
};
return {
count,
increment,
double,
};
},
};
</script>
(2) 可以将 getter 与 setter 进行拆分,实现取值与赋值处理
<template>
<div class="about">
<h2>count: {{ count }}</h2>
<h2>double: {{ double }}</h2>
<h2>double2: {{ double2 }}</h2>
<hr />
<button @click="increment">加1</button>
</div>
</template>
<script lang="ts">
import { computed, ref } from 'vue';
export default {
setup() {
const count = ref(0);
const double = computed(() => {
console.log('double computed', count.value);
return count.value * 2;
});
// 包含getter与setter的计算属性
const double2 = computed({
get() {
return count.value * 2;
},
set(value: number) {
count.value = value / 2;
},
});
const increment = () => {
count.value++;
setTimeout(() => {
// 调用computed的setter操作
double2.value += 2;
}, 1000);
};
return {
count,
increment,
double,
double2,
};
},
};
</script>
4) reactive
接收一个普通对象然后返回该普通对象的响应式代理器对象
响应式转换是“深层的”:会影响对象内部所有嵌套的属性
基于 ES2015 的 Proxy 实现,通过代理对象操作源对象内部数据都是响应式的
:::tip reactive 区别 ref
-
ref 的作用就是将一个原始数据类型(primitive data type)转换成一个带有响应式特性(reactivity)的数据类型,原始数据类型共有 7 个,分别是:String、Number、Boolean、Null、Undefined、Symbol、BigInt
-
ref 与 reactive 定义基本元素类型数据时,ref 定义的是包装后的响应式数据,而 reactive 定义的还是原来的类型(也就是 reactive 定义基本类型不是响应式的,修改数据不能更新到模板)
-
使用 ref 还是 reactive 可以选择这样的准则
第一:像平常写普通的 js 代码,选择原始类型和对象类型一样来选择是使用 ref 还是 reactive。
第二:所有场景都使用 reactive,但是要记得使用 toRefs 保证 reactive 对象属性保持响应性。
:::
(1) reactive 设置对象并进行插值显示
<template>
<div class="about">
<p>msg:{{ state.msg }}</p>
<p>numbers:{{ state.numbers }}</p>
<p>name:{{ state.person.name }}</p>
</div>
</template>
<script lang="ts">
import { reactive } from "vue";
export default {
setup() {
// state 对象是reactive中原始对象的代理对象
// 一旦操作代理对象的属性,内部操作的是原始对象的属性
const state = reactive({
msg: "hello Vue3",
numbers: [1, 2, 3],
person: {
name: "Vane",
},
});
return {
state,
};
},
};
</script>
(2) reactive 对于数据的更新处理
<template>
<div class="about">
<p>msg:{{ state.msg }}</p>
<p>numbers:{{ state.numbers }}</p>
<p>name:{{ state.person.name }}</p>
<button @click="state.update">update</button>
</div>
</template>
<script lang="ts">
import { reactive } from "vue";
export default {
setup() {
// state 对象是reactive中原始对象的代理对象
// 一旦操作代理对象的属性,内部操作的是原始对象的属性
const state = reactive({
msg: "hello Vue3",
numbers: [1, 2, 3],
person: {
name: "Vane",
},
update: () => {
state.msg = "Vue3 is very good";
// 通过下标直接替换数组元素
// Vue3会自动更新,Vue2中不会自动更新,需要用转换后的数组函数
state.numbers[1] = 100;
// 通过路径直接替换对象属性
// Vue3会自动更新,Vue2中需要利用Vue.set进行对象属性更新
state.person.name = "Chinavane";
},
});
return {
state,
};
},
};
</script>
(3) 解构 reactive 以后产生的问题,注意 numbers 与 person 内容进行更新的模式切换
<template>
<div class="about">
<p>msg:{{ msg }}</p>
<p>numbers:{{ numbers }}</p>
<p>name:{{ person.name }}</p>
<button @click="update">update</button>
</div>
</template>
<script lang="ts">
import { reactive } from "vue";
export default {
setup() {
// state 对象是reactive中原始对象的代理对象
// 一旦操作代理对象的属性,内部操作的是原始对象的属性
const state = reactive({
msg: "hello Vue3",
numbers: [1, 2, 3],
person: {
name: "Vane",
},
update: () => {
state.msg = "Vue3 is very good";
// 通过下标直接替换数组元素
// Vue3会自动更新,Vue2中不会自动更新,需要用转换后的数组函数
state.numbers = [1, 100, 3];
// 通过路径直接替换对象属性
// Vue3会自动更新,Vue2中需要利用Vue.set进行对象属性更新
state.person = { name: "Chinavane" };
},
});
return {
// 问题:如果 reactive 响应式对象一旦解析出来,其属性则不再是响应式属性
...state,
};
},
};
</script>
(4) 利用 toRefs 将 reactive 中的每个属性转成 ref 响应式数据对象
...
<script lang="ts">
import { reactive, toRefs } from "vue";
export default {
setup() {
...
return {
// 问题:如果 reactive 响应式对象一旦解析出来,其属性则不再是响应式属性
// 解决:利用 toRefs 将 reactive 中的每个属性转成 ref 响应式数据对象
...toRefs(state),
};
},
};
</script>
(5) 利用接口对数据类型内容进行约束
...
<script lang="ts">
import { reactive, toRefs } from "vue";
interface State {
msg: string;
numbers: number[];
person: {
name?: string;
};
update: () => void;
}
export default {
setup() {
const state: State = reactive({
...
});
return {
...toRefs(state),
};
},
};
</script>
5) toRefs
问题: reactive 对象取出的所有属性值都是非响应式的
解决: 利用 toRefs 可以将一个响应式 reactive 对象的所有原始属性转换为响应式的 ref 属性
6) watch
与选项 API this.$watch (以及相应的 watch 选项) 完全等效
侦听一个数据: ref 或 reactive 属性值(getter 函数)
侦听多个数据
(1) 设置 ref 值 count,并进行数据更新
<template>
<div class="about">
...
<p>count:{{ count }}</p>
<button @click="update">update</button>
</div>
</template>
<script lang="ts">
import { reactive, ref, toRefs } from "vue";
...
export default {
setup() {
const count = ref(0);
const state: State = reactive({
...
update: () => {
...
count.value++;
},
});
return {
...toRefs(state),
count,
};
},
};
</script>
(2) 监控一个 ref 的值
<script lang="ts">
...
export default {
...
// 监视一个ref值
watch(count, (newVal, oldVal) => {
console.log("watch count", newVal, oldVal);
document.title = count.value.toString();
});
return {
...toRefs(state),
count,
};
},
};
</script>
(3) 监视 reactive 对象中的某个属性,需要用箭头函数的方式指向监控对象
<script lang="ts">
...
export default {
...
// 监视 reactive 对象中的某个属性,注意:指定返回它的getter
watch(
() => state.msg,
(newVal, oldVal) => {
console.log("watch msg", newVal, oldVal);
}
);
return {
...toRefs(state),
count,
};
},
};
</script>
(4) 监视多个对象
export default {
...
// 监视多个对象,利用解构方式输出
watch(
[count, () => state.msg],
([countNewVal, msgNewVal], [countOldValue, msgOldVal]) => {
console.log("watch 多值解构count", countNewVal, countOldValue);
console.log("watch 多值解构msg", msgNewVal, msgOldVal);
}
);
// 监视多个对象,利用单一对象输出
const stateRef = toRefs(state);
watch([count, () => state.msg, stateRef.msg, state], (values) => {
console.log("watch 多值", values);
});
return {
...stateRef,
count,
};
},
};
</script>
4.比较 Vue2 与 Vue3 的响应式(重要)
1) vue2 的响应式
- 核心
- 对象:通过 defineProperty 对对象的已有属性值的读取和修改进行劫持(监视/拦截)
- 数组:通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
Object.defineProperty(data, 'count', {
get() {},
set() {},
});
- 问题
- 对象直接新添加的属性或删除已有属性, 界面不会自动更新
- 直接通过下标替换元素或更新 length, 界面不会自动更新
- Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象。
- Object.defineProperty 本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>介绍Object.defineProperty的作用</title>
</head>
<body>
<script>
const obj = {
name: '张三',
age: 18,
};
// 取值操作
console.log(obj.name);
// 赋值操作
obj.name = '李四';
console.log(obj.name);
// 通过Object.defineProperty可以拦截取值赋值属性操作
Object.defineProperty(obj, 'name', {
enumerable: true, // 当前属性允许被循环
// 如果不允许for-in的时候会被跳过
configurable: true, // 当前属性允许被配置
get() {
// getter
console.log('有人获取了obj.name的值');
return '我不是张三';
},
set(newVal) {
// setter
console.log('我不要你给的值', newVal);
},
});
console.log(obj.name); // get的演示
obj.name = '李四'; // set的演示
console.log(obj);
</script>
</body>
</html>
2) Vue3 的响应式
- 核心
- 通过 Proxy(代理):拦截对 data 任意属性的任意(13 种)操作, 包括属性值的读写, 属性的添加, 属性的删除等…
- 通过 Reflect(反射):动态对代理对象的相应属性进行特定的操作
- 文档
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
- Proxy 不仅可以代理对象,还可以代理数组,还可以代理动态增加的属性。
new Proxy(data, {
// 拦截读取属性值
get(target, prop) {
return Reflect.get(target, prop);
},
// 拦截设置属性值或添加新属性
set(target, prop, value) {
return Reflect.set(target, prop, value);
},
// 拦截删除属性
deleteProperty(target, prop) {
return Reflect.deleteProperty(target, prop);
},
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Proxy 与 Reflect</title>
</head>
<body>
<script>
const user = {
name: 'Vane',
age: 42,
};
/*
proxyUser是代理对象, user是被代理对象
后面所有的操作都是通过代理对象来操作被代理对象内部属性
*/
const proxyUser = new Proxy(user, {
get(target, prop) {
console.log('劫持get()', prop);
return Reflect.get(target, prop);
},
set(target, prop, val) {
console.log('劫持set()', prop, val);
return Reflect.set(target, prop, val); // (2)
},
deleteProperty(target, prop) {
console.log('劫持delete属性', prop);
return Reflect.deleteProperty(target, prop);
},
});
// 读取属性值
console.log(proxyUser === user);
console.log(proxyUser.name, proxyUser.age);
// 设置属性值
proxyUser.name = 'Chinavane';
proxyUser.age = 18;
console.log(user);
// 添加属性
proxyUser.sex = '男';
console.log(user);
// 删除属性
delete proxyUser.sex;
console.log(user);
</script>
</body>
</html>
5.生命周期
1) vue2.x 的生命周期
2) vue3 的生命周期
3) 选项 API 生命周期选项和组合 API 之间的映射
Vue3 | Vue2 |
---|---|
use setup() | |
use setup() | |
onBeforeMount | beforeMount |
onMounted | mounted |
onBeforeUpdate | beforeUpdate |
onUpdated | updated |
onBeforeUnmount | beforeDestroy |
onUnmounted | destroyed |
onErrorCaptured | errorCaptured |
onRenderTracked | renderTracked |
onRenderTriggered | renderTriggered |
<template>
<div class="about">
<h2>msg: {{ msg }}</h2>
<hr />
<button @click="update">更新</button>
</div>
</template>
<script lang="ts">
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from 'vue';
export default {
beforeCreate() {
console.log('beforeCreate()');
},
created() {
console.log('created');
},
beforeMount() {
console.log('beforeMount');
},
mounted() {
console.log('mounted');
},
beforeUpdate() {
console.log('beforeUpdate');
},
updated() {
console.log('updated');
},
beforeUnmount() {
console.log('beforeUnmount');
},
unmounted() {
console.log('unmounted');
},
setup() {
const msg = ref('abc');
const update = () => {
msg.value += '--';
};
onBeforeMount(() => {
console.log('--onBeforeMount');
});
onMounted(() => {
console.log('--onMounted');
});
onBeforeUpdate(() => {
console.log('--onBeforeUpdate');
});
onUpdated(() => {
console.log('--onUpdated');
});
onBeforeUnmount(() => {
console.log('--onBeforeUnmount');
});
onUnmounted(() => {
console.log('--onUnmounted');
});
return {
msg,
update,
};
},
};
</script>
4) 自定义 hooks 函数
-
作用:对多个组件重复的功能进行提取封装
-
在 vue2 中,可以使用 mixin 技术,在 vue3 中使用自定义 hooks 函数
需求 1: 收集用户鼠标点击的页面坐标
(1) 在组件中进行功能的实现
<template>
<div class="about">
<p>x:{{ x }}</p>
<p>y:{{ y }}</p>
</div>
</template>
<script lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
// 初始化坐标数据
const x = ref(-1);
const y = ref(-1);
// 用于收集点击事件坐标的函数
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX;
y.value = e.pageY;
};
// 挂载后绑定点击监听
onMounted(() => {
document.addEventListener('click', updatePosition);
});
// 卸载前解绑点击监听
onUnmounted(() => {
document.removeEventListener('click', updatePosition);
});
return { x, y };
},
};
</script>
(2) 将功能抽离成自定义的 hooks
hooks/useMousePosition.ts
/*
自定义hooks: 收集用户鼠标点击的页面坐标
*/
import { ref, onMounted, onUnmounted } from 'vue';
export default function useMousePosition() {
// 初始化坐标数据
const x = ref(-1);
const y = ref(-1);
// 用于收集点击事件坐标的函数
const updatePosition = (e: MouseEvent) => {
x.value = e.pageX;
y.value = e.pageY;
};
// 挂载后绑定点击监听
onMounted(() => {
document.addEventListener('click', updatePosition);
});
// 卸载前解绑点击监听
onUnmounted(() => {
document.removeEventListener('click', updatePosition);
});
return { x, y };
}
(3) 在组件中调用自定义钩子函数
<template>
<div class="about">
<p>x:{{ x }}</p>
<p>y:{{ y }}</p>
</div>
</template>
<script lang="ts">
import useMousePosition from '../hooks/useMousePosition';
export default {
setup() {
const { x, y } = useMousePosition();
return { x, y };
},
};
</script>
需求 2:封装异步请求
(1) 将功能抽离成自定义的 hooks
hooks/useUrlLoader.ts
/*
使用axios发送异步ajax请求
*/
import { ref } from 'vue';
import axios from 'axios';
export default function useUrlLoader(url: string) {
const result = ref(null);
const loading = ref(true);
const errorMsg = ref(null);
axios
.get(url)
.then((response) => {
loading.value = false;
result.value = response.data;
})
.catch((e) => {
loading.value = false;
errorMsg.value = e.message || '未知错误';
});
return {
loading,
result,
errorMsg,
};
}
(2) 在组件中调用自定义钩子函数
<template>
<div class="about">
<p>x:{{ x }}</p>
<p>y:{{ y }}</p>
<hr />
<p v-if="loading">Loading...</p>
<p v-else-if="errorMsg">{{ errorMsg }}</p>
<p v-else>{{ result }}</p>
</div>
</template>
<script lang="ts">
import useMousePosition from '../hooks/useMousePosition';
import useUrlLoader from '../hooks/useUrlLoader';
export default {
setup() {
const { x, y } = useMousePosition();
const { result, loading, errorMsg } = useUrlLoader(
'https://dog.ceo/api/breeds/image/random'
);
return { x, y, result, loading, errorMsg };
},
};
</script>
6. 利用 TS 强化类型检查与提示
1) 使用泛型
hooks/useUrlLoader.ts
export default function useUrlLoader<T>(url: string) {
const result = ref<T | null>(null)
...
}
2) 定义接口,约束信息,单一对象
...
<script lang="ts">
import { watch } from "vue";
import useMousePosition from "../hooks/useMousePosition";
import useUrlLoader from "../hooks/useUrlLoader";
interface DogResult {
message: string;
status: string;
}
export default {
setup() {
const { x, y } = useMousePosition();
const { result, loading, errorMsg } = useUrlLoader<DogResult>(
"https://dog.ceo/api/breeds/image/random"
);
watch(result, (newVal, oldVal) => {
console.log(result.value?.message); // 原来没有使用接口类型约束没有提示,有接口类型则可以提示
});
return { x, y, result, loading, errorMsg };
},
};
</script>
3) 定义接口,约束信息,数组对象
...
<script lang="ts">
import { watch } from "vue";
import useMousePosition from "../hooks/useMousePosition";
import useUrlLoader from "../hooks/useUrlLoader";
interface CatResult {
breeds: any[];
id: string;
url: string;
width: number;
height: number;
}
export default {
setup() {
const { x, y } = useMousePosition();
const { result, loading, errorMsg } = useUrlLoader<CatResult[]>(
"https://api.thecatapi.com/v1/images/search"
);
watch(result, (newVal, oldVal) => {
if (result.value) {
console.log(result.value[0].url);
}
});
return { x, y, result, loading, errorMsg };
},
};
</script>
7.使用 defineComponent 包裹组件
- 问题: 配置选项没有提示
- 解决: 使用 defineComponent(options)
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true,
},
},
setup(props, context) {
console.log(props.msg);
console.log(context.attrs, context.slots, context.emit);
return {};
},
});
</script>
eds/image/random"
);
watch(result, (newVal, oldVal) => {
console.log(result.value?.message); // 原来没有使用接口类型约束没有提示,有接口类型则可以提示
});
return { x, y, result, loading, errorMsg };
},
};
### 3) 定义接口,约束信息,数组对象
```typescript {7-13,18-26}
...
<script lang="ts">
import { watch } from "vue";
import useMousePosition from "../hooks/useMousePosition";
import useUrlLoader from "../hooks/useUrlLoader";
interface CatResult {
breeds: any[];
id: string;
url: string;
width: number;
height: number;
}
export default {
setup() {
const { x, y } = useMousePosition();
const { result, loading, errorMsg } = useUrlLoader<CatResult[]>(
"https://api.thecatapi.com/v1/images/search"
);
watch(result, (newVal, oldVal) => {
if (result.value) {
console.log(result.value[0].url);
}
});
return { x, y, result, loading, errorMsg };
},
};
</script>
7.使用 defineComponent 包裹组件
- 问题: 配置选项没有提示
- 解决: 使用 defineComponent(options)
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'HelloWorld',
props: {
msg: {
type: String,
required: true,
},
},
setup(props, context) {
console.log(props.msg);
console.log(context.attrs, context.slots, context.emit);
return {};
},
});
</script>