539 lines
16 KiB
Markdown
539 lines
16 KiB
Markdown
---
|
||
title: Vue3
|
||
date: 2023-11-18 10:30:31
|
||
author: 文永达
|
||
---
|
||
# 响应式
|
||
|
||
在Vue 3中,响应式是通过`reactive`函数和`ref`函数实现的。`reactive`函数用于创建一个响应式对象,而`ref`函数用于创建一个包装对象,将基本类型的值转换为响应式对象。
|
||
|
||
具体来说,`reactive`函数接收一个普通对象作为参数,并返回一个响应式对象,如下所示:
|
||
|
||
```ts
|
||
import { reactive } from 'vue';
|
||
|
||
const state = reactive({
|
||
count: 0
|
||
});
|
||
```
|
||
|
||
在这个例子中,我们使用`reactive`函数创建了一个响应式对象`state`,它包含一个属性`count`,初始值为0。
|
||
|
||
在组件中,你可以使用`setup`函数来访问响应式对象,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<p>Count: {{ state.count }}</p>
|
||
<button @click="increment">Increment</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { reactive } from 'vue';
|
||
|
||
export default {
|
||
setup() {
|
||
const state = reactive({
|
||
count: 0
|
||
});
|
||
|
||
const increment = () => {
|
||
state.count++;
|
||
};
|
||
|
||
return {
|
||
state,
|
||
increment
|
||
};
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
在这个例子中,我们在`setup`函数中访问了响应式对象`state`,并将它绑定到模板中。当点击按钮时,我们调用`increment`函数来增加`state.count`的值。由于`state`是响应式对象,当`state.count`的值发生变化时,模板中的内容也会自动更新。
|
||
|
||
除了`reactive`函数外,Vue 3还提供了`ref`函数,用于将基本类型的值转换为响应式对象。例如,你可以使用`ref`函数来创建一个响应式的计数器,如下所示:
|
||
|
||
```ts
|
||
import { ref } from 'vue';
|
||
|
||
const count = ref(0);
|
||
```
|
||
|
||
在这个例子中,我们使用`ref`函数创建了一个响应式对象`count`,它的初始值为0。
|
||
|
||
在组件中,你可以使用`setup`函数来访问响应式对象,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<p>Count: {{ count }}</p>
|
||
<button @click="increment">Increment</button>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref } from 'vue';
|
||
|
||
export default {
|
||
setup() {
|
||
const count = ref(0);
|
||
|
||
const increment = () => {
|
||
count.value++;
|
||
};
|
||
|
||
return {
|
||
count,
|
||
increment
|
||
};
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
在这个例子中,我们在`setup`函数中访问了响应式对象`count`,并将它绑定到模板中。当点击按钮时,我们调用`increment`函数来增加`count.value`的值。由于`count`是响应式对象,当`count.value`的值发生变化时,模板中的内容也会自动更新。
|
||
|
||
总之,Vue 3中的响应式是通过`reactive`函数和`ref`函数实现的。`reactive`函数用于创建一个响应式对象,而`ref`函数用于将基本类型的值转换为响应式对象。你可以在`setup`函数中访问响应式对象,并将它们绑定到模板中。
|
||
|
||
# 依赖注入
|
||
|
||
Vue 3中的依赖注入是通过provide和inject实现的。provide和inject是一对API,用于在父组件和子组件之间共享数据。具体来说,provide用于在父组件中提供数据,而inject用于在子组件中注入数据。
|
||
|
||
在父组件中,你可以使用provide来提供数据,如下所示:
|
||
|
||
```vue
|
||
<script>
|
||
import { provide } from 'vue';
|
||
|
||
export default {
|
||
setup() {
|
||
const data = 'Hello, World!';
|
||
provide('data', data);
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
在子组件中,你可以使用inject来注入数据,如下所示:
|
||
|
||
```vue
|
||
<script>
|
||
import { inject } from 'vue';
|
||
|
||
export default {
|
||
setup() {
|
||
const data = inject('data');
|
||
console.log(data); // 输出:Hello, World!
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
在这个例子中,父组件提供了一个名为'data'的数据,子组件通过inject来注入这个数据。注意,子组件中的inject必须在setup函数中使用,而且必须在模板中使用才能生效。
|
||
|
||
总之,Vue 3中的依赖注入是通过provide和inject实现的,它可以帮助你在父组件和子组件之间共享数据。
|
||
|
||
# 插槽
|
||
|
||
## 默认插槽
|
||
|
||
Vue 3中的插槽是通过`<slot>`元素实现的。插槽允许你在组件中定义一些占位符,然后在使用组件时填充这些占位符。具体来说,你可以在组件中使用`<slot>`元素来定义插槽,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<h2>{{ title }}</h2>
|
||
<slot></slot>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们定义了一个名为`title`的属性和一个插槽。插槽使用了`<slot>`元素,它表示这里是一个插槽占位符,可以在使用组件时填充内容。
|
||
|
||
在使用组件时,你可以在组件标签中填充内容,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<my-component title="Hello, World!">
|
||
<p>This is the content of the slot.</p>
|
||
</my-component>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们使用了`<my-component>`组件,并在组件标签中填充了一个`<p>`元素。这个`<p>`元素就是填充了组件中定义的插槽。
|
||
|
||
除了默认插槽外,Vue 3还支持具名插槽和作用域插槽。具名插槽允许你定义多个插槽,并在使用组件时指定要填充哪个插槽。作用域插槽允许你将数据传递给插槽内容,以便在插槽中使用。
|
||
|
||
总之,Vue 3中的插槽是通过`<slot>`元素实现的,它允许你在组件中定义占位符,并在使用组件时填充内容。除了默认插槽外,Vue 3还支持具名插槽和作用域插槽。
|
||
|
||
## 具名插槽
|
||
|
||
在Vue 3中,具名插槽允许你定义多个插槽,并在使用组件时指定要填充哪个插槽。具名插槽可以通过在`<slot>`元素上添加`name`属性来定义,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<slot name="header"></slot>
|
||
<slot></slot>
|
||
<slot name="footer"></slot>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们定义了三个插槽,其中第一个和第三个是具名插槽,分别使用了`name`属性来定义。第二个插槽没有使用`name`属性,它是默认插槽。
|
||
|
||
在使用组件时,你可以使用`v-slot`指令来指定要填充哪个插槽,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<my-component>
|
||
<template v-slot:header>
|
||
<h1>This is the header slot.</h1>
|
||
</template>
|
||
<p>This is the default slot.</p>
|
||
<template v-slot:footer>
|
||
<p>This is the footer slot.</p>
|
||
</template>
|
||
</my-component>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们使用了`<my-component>`组件,并在组件标签中填充了三个插槽。使用`v-slot`指令来指定要填充哪个插槽,语法为`v-slot:name`,其中`name`是插槽的名称。
|
||
|
||
除了`v-slot`指令外,Vue 3还支持另一种简写语法`#`,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<my-component>
|
||
<template #header>
|
||
<h1>This is the header slot.</h1>
|
||
</template>
|
||
<p>This is the default slot.</p>
|
||
<template #footer>
|
||
<p>This is the footer slot.</p>
|
||
</template>
|
||
</my-component>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们使用了`#`来代替`v-slot`指令,语法更加简洁。
|
||
|
||
总之,Vue 3中的具名插槽允许你定义多个插槽,并在使用组件时指定要填充哪个插槽。你可以使用`v-slot`指令或`#`来指定要填充哪个插槽。
|
||
|
||
## 作用域插槽
|
||
|
||
在Vue 3中,作用域插槽允许你将数据传递给插槽内容,以便在插槽中使用。作用域插槽可以通过在`<slot>`元素上添加`v-bind`指令来定义,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<slot v-bind:user="user"></slot>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们定义了一个作用域插槽,并使用了`v-bind`指令来将`user`数据传递给插槽内容。
|
||
|
||
在使用组件时,你可以在插槽标签中使用`v-slot`指令来定义插槽,并在插槽内容中使用传递的数据,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<my-component>
|
||
<template v-slot:default="slotProps">
|
||
<p>{{ slotProps.user.name }}</p>
|
||
<p>{{ slotProps.user.age }}</p>
|
||
</template>
|
||
</my-component>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们使用了`<my-component>`组件,并在组件标签中填充了一个作用域插槽。使用`v-slot`指令来定义插槽,并使用`default`作为插槽的名称。在插槽内容中,我们可以通过`slotProps`来访问传递的数据。
|
||
|
||
除了`v-slot`指令外,Vue 3还支持另一种简写语法`#`,如下所示:
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<my-component>
|
||
<template #default="slotProps">
|
||
<p>{{ slotProps.user.name }}</p>
|
||
<p>{{ slotProps.user.age }}</p>
|
||
</template>
|
||
</my-component>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
在这个例子中,我们使用了`#`来代替`v-slot`指令,语法更加简洁。
|
||
|
||
总之,Vue 3中的作用域插槽允许你将数据传递给插槽内容,以便在插槽中使用。你可以在`<slot>`元素上使用`v-bind`指令来定义作用域插槽,并在插槽标签中使用`v-slot`指令或`#`来定义插槽,并在插槽内容中使用传递的数据。
|
||
|
||
# 比较重要的知识点总结
|
||
|
||
## 对vue.js的响应式数据的理解
|
||
|
||
### Vue 2.x
|
||
|
||
**对象类型**:通过`object.defineProperty()`对属性的读取、修改进行拦截(数据劫持)
|
||
|
||
**数组类型**:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)
|
||
|
||

|
||
|
||
- 当你把一个普通的JavaScript对象传入Vue实例作为**data**选项,Vue将遍历此对象所有的property,并使用**Object.defineProperty**把这些property全部转为**getter/setter**。
|
||
|
||
- **Object.defineProperty**是ES5中一个无法shim的特性,这也就是Vue不支持IE8以及更低版本浏览器的原因。
|
||
|
||
- 当使用这些数据属性是,会进行依赖收集(收集到当前组件的watcher)
|
||
- 每个组件都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据记录为依赖之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染
|
||
|
||
**存在问题**:
|
||
|
||
- 新增属性、删除属性,界面不会更新
|
||
- 直接通过下标修改数组,界面不会自动更新
|
||
|
||
### Vue 3.x
|
||
|
||
**通过Proxy(代理)**:拦截对象中任意属性的变化,包括:属性值的读写,属性的增加,属性的删除等。
|
||
|
||
**通过Reffect(反射)**:对源对象的属性进行操作
|
||
|
||
`Proxy`是 ECMAScript 6 中新增的属性。
|
||
|
||
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
|
||
|
||
被Proxy代理虚拟化的对象。它常被作为代理得存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)。
|
||
|
||
语法:
|
||
|
||
```javascript
|
||
const p = new Proxy(target, handler)
|
||
```
|
||
|
||
参数:
|
||
|
||
target:
|
||
|
||
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
|
||
|
||
handler:
|
||
|
||
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
|
||
|
||
# 组合式函数(Hook)
|
||
|
||
## 概述
|
||
|
||
在 Vue 应用的概念中,“组合式函数”(Composables)是一个利用 Vue 的组合式 API 来封装和复用**有状态逻辑**的函数。
|
||
|
||
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了**无状态的逻辑**:它在接受一些输入后立刻返回所期望的输出。复用无状态逻辑的库很多,比如你可能已经用过的lodash或是date-fns。
|
||
|
||
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
|
||
|
||
## 约定和最佳实践
|
||
|
||
### 命名
|
||
|
||
组合式函数约定用驼峰命名法命名,并以“use”作为开头。
|
||
|
||
### 输入参数
|
||
|
||
即便不依赖于 ref 或 getter 的响应性,组合式函数也可以接受它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用`toValue()`工具函数来实现:
|
||
|
||
```js
|
||
import { toValue } from 'vue'
|
||
|
||
function useFeature(maybeRefOrGetter) {
|
||
// 如果 maybeRefOrGetter 是一个 ref 或 getter,
|
||
// 将返回它的规范化值。
|
||
// 否则原样返回。
|
||
const value = toValue(maybeRefOrGetter)
|
||
}
|
||
```
|
||
|
||
如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它能够被正确追踪,请确保要么使用`watch()`显式地监视 ref 或 getter,要么在`watchEffect()`中调用`toValue()`。
|
||
|
||
## 封装下拉框Hook
|
||
|
||
```ts
|
||
import { onMounted, reactive, ref } from 'vue';
|
||
// 定义下拉框接收的数据格式
|
||
export interface SelectOption {
|
||
value: string;
|
||
label: string;
|
||
disabled?: boolean;
|
||
key?: string;
|
||
}
|
||
// 定义入参格式
|
||
interface FetchSelectProps {
|
||
apiFun: () => Promise<any[]>;
|
||
}
|
||
|
||
export function useFetchSelect(props: FetchSelectProps) {
|
||
const { apiFun } = props;
|
||
|
||
const options = ref<SelectOption[]>([]);
|
||
|
||
const loading = ref(false);
|
||
|
||
/* 调用接口请求数据 */
|
||
const loadData = () => {
|
||
loading.value = true;
|
||
options.value = [];
|
||
return apiFun().then(
|
||
(data) => {
|
||
loading.value = false;
|
||
options.value = data;
|
||
return data;
|
||
},
|
||
(err) => {
|
||
// 未知错误,可能是代码抛出的错误,或是网络错误
|
||
loading.value = false;
|
||
options.value = [
|
||
{
|
||
value: '-1',
|
||
label: err.message,
|
||
disabled: true,
|
||
},
|
||
];
|
||
// 接着抛出错误
|
||
return Promise.reject(err);
|
||
}
|
||
);
|
||
};
|
||
|
||
// onMounted 中调用接口
|
||
onMounted(() => {
|
||
loadData();
|
||
});
|
||
|
||
return reactive({
|
||
options,
|
||
loading,
|
||
});
|
||
}
|
||
```
|
||
|
||
组件调用
|
||
|
||
```vue
|
||
<script setup name="DDemo" lang="ts">
|
||
import { useFetchSelect } from './hook';
|
||
|
||
// 模拟调用接口
|
||
function getRemoteData() {
|
||
return new Promise<any[]>((resolve, reject) => {
|
||
setTimeout(() => {
|
||
// 模拟接口调用有概率出错
|
||
if (Math.random() > 0.5) {
|
||
resolve([
|
||
{
|
||
key: 1,
|
||
name: '苹果',
|
||
value: 1,
|
||
},
|
||
{
|
||
key: 2,
|
||
name: '香蕉',
|
||
value: 2,
|
||
},
|
||
{
|
||
key: 3,
|
||
name: '橘子',
|
||
value: 3,
|
||
},
|
||
]);
|
||
} else {
|
||
reject(new Error('不小心出错了!'));
|
||
}
|
||
}, 3000);
|
||
});
|
||
}
|
||
|
||
// 将之前用的 options,loading,和调用接口的逻辑都抽离到hook中
|
||
const selectBind = useFetchSelect({
|
||
apiFun: getRemoteData,
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<!-- 将hook返回的接口,通过 v-bind 绑定给组件 -->
|
||
<a-select v-bind="selectBind" />
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
## Loading状态hook
|
||
|
||
```ts
|
||
import { Ref, ref } from 'vue';
|
||
|
||
type TApiFun<TData, TParams extends Array<any>> = (...params: TParams) => Promise<TData>;
|
||
|
||
interface AutoRequestOptions {
|
||
// 定义一下初始状态
|
||
loading?: boolean;
|
||
// 接口调用成功时的回调
|
||
onSuccess?: (data: any) => void;
|
||
}
|
||
|
||
type AutoRequestResult<TData, TParams extends Array<any>> = [Ref<boolean>, TApiFun<TData, TParams>];
|
||
|
||
/* 控制loading状态的自动切换hook */
|
||
export function useAutoRequest<TData, TParams extends any[] = any[]>(fun: TApiFun<TData, TParams>, options?: AutoRequestOptions): AutoRequestResult<TData, TParams> {
|
||
const { loading = false, onSuccess } = options || { loading: false };
|
||
|
||
const requestLoading = ref(loading);
|
||
|
||
const run: TApiFun<TData, TParams> = (...params) => {
|
||
requestLoading.value = true;
|
||
return fun(...params)
|
||
.then((res) => {
|
||
onSuccess && onSuccess(res);
|
||
return res;
|
||
})
|
||
.finally(() => {
|
||
requestLoading.value = false;
|
||
});
|
||
};
|
||
|
||
return [requestLoading, run];
|
||
}
|
||
```
|
||
|
||
组件调用
|
||
|
||
```vue
|
||
<script setup name="Index" lang="ts">
|
||
import { useAutoRequest } from "./hook";
|
||
import { Button } from "ant-design-vue";
|
||
import { submitApi } from "@/api";
|
||
|
||
const [loading, submit] = useAutoRequest(submitApi);
|
||
|
||
function onSubmit() {
|
||
submit("aaa").then((res) => {
|
||
console.log("res", res);
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="col">
|
||
<Button :loading="loading" @click="onSubmit">提交</Button>
|
||
</div>
|
||
</template>
|
||
```
|
||
|