引言

https://www.vue3js.cn/docs/zh/guide/installation.html

组合式 API?

  • 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
29
30
31
32
33
34
35
36
37
38
export default {
data() {
return {
count: 0, // 1
searchText: "", // 2
};
},
components: {},
watch: {
count: (newVal, oldVal) => {
//...... 1
},
searchText: (newVal, oldVal) => {
//...... 2
},
},
computed: {
countAdd() {
return this.count + 1;
}, // 1
searchAdd() {
return this.searchAdd + "add";
}, // 2
},
methods: {
countClick() {
// 1
this.count + 1;
},
searchFilter() {
// 2
//.........
},
},
mounted() {
console.log("Hi Vue2");
},
};

用组件的选项 (data、computed、methods、watch) 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。看上面的 demo 中 1 的逻辑与 2 交叉编写,很难一次找到一套功能的所有逻辑。在看下面这个组件事例:

一个大型组件的事例,逻辑关注的是所对应各个颜色,将每一个组件所对应的每一套逻辑梳理出来并且能够记住,这本就是我们不应该费很大精力去关注的,但是现在你必须得一个组件几百行,穿插找到属于你想要的那几行,如果我们能够将同一个逻辑的关注点放在一个地方会更好,而这个正是组合 api 使我们能够做到的。

  • 组合式 api 基础 setup

setup 选项应该是一个接受 props 和 context 的函数,我们将在稍后讨论。此外,我们从 setup 返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

先了解一下两个参数

  1. props
    setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。
    如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来安全地完成此操作。直接使用 es6 进行解构,可能会造成消除 props 的响应性。
1
2
3
4
5
6
7
import { toRefs } from 'vue'

setup(props) {
const { title } = toRefs(props)

console.log(title.value)
}
  1. context
    传递给 setup 函数的第二个参数是 context。context 是一个普通的 JavaScript 对象,它暴露三个组件的 property:
1
2
3
4
5
6
7
8
9
10
11
12
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs);

// 插槽 (非响应式对象)
console.log(context.slots);

// 触发事件 (方法)
console.log(context.emit);
},
};

attrs 和 slots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x 或 slots.x 的方式引用 property。请注意,与 props 不同,attrs 和 slots 是非响应式的。如果你打算根据 attrs 或 slots 更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。

在 setup() 内部,this 不会是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这在和其它选项式 API 一起使用 setup() 时可能会导致混淆。
现在用 setup 将上面的 demo 给重构一下

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 { reactive, watch, computed, onMounted, ref } from "vue";

export default {
setup(props, context) {
// 1
const count = ref(0);
watch(count, (newVal, oldVal) => {});
const countAdd = computed(() => count.value++);
const countClick = () => count.value++;
// 2
const searchText = ref("");
watch(searchText, (newVal, oldVal) => {});
const searchAdd = computed(() => searchText.value + "add");
const searchFilter = () => {};

onMounted(() => {
console.log("Hi Vue3");
});

return {
count,
countAdd,
countClick,
searchText,
searchAdd,
searchFilter,
};
},
};

可是我觉得这样写还是不太满意,setup 中好多代码,看着就比较乱,虽然逻辑在一块,但是逻辑堆的太多了可读性就不够了,所以我们可以使用 hooks 来设计组件,提高代码的复用性。可读性也比较好。那么既然要用 hooks 自然也是要遵守 hooks 协议,我们以 use 开头:

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
// ./hooks.js
const useCount = (val) => {
const count = ref(val);
watch(count, (newVal, oldVal) => {});
const countAdd = computed(() => count.value++);
const countClick = () => count.value++;
return {
count,
countAdd,
countClick,
};
};

const useSearch = (val) => {
const searchText = ref("");
watch(searchText, (newVal, oldVal) => {});
const searchAdd = computed(() => searchText.value + "add");
const searchFilter = () => {};
return {
searchText,
searchAdd,
searchFilter,
};
};

// App.vue
export default {
setup(props, context) {
// 1
const { count, countAdd, countClick } = useCount(0);
// 2
const { searchText, searchAdd, searchFilter } = useSearch("");

onMounted(() => {
console.log("Hi Vue3");
});

return {
count,
countAdd,
countClick,
searchText,
searchAdd,
searchFilter,
};
},
};

这样一看,组件代码简洁,代码复用性比较高,我哪里用到这个功能,我引入 hooks,调用一波,节省了 cv 代码时间,而且耦合度比较低,那个功能出错了,我们只需要修改对应的 hooks,不用从上排查到下了。

生命周期

选项式 API Hooks APi
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。如同我们上面例子的 onMounted 一样,钩子函数被组件调用会立即执行。

provide/inject

在 setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 时来定义每个 property。

provide 函数允许你通过两个参数定义 property:

property 的 name ( 类型)
property 的 value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import About from "./About";
import { provide, ref, reactive } from "vue";
export default {
components: {
About,
},
setup() {
const location = ref("North Pole");
const geolocation = reactive({
longitude: 90,
latitude: 135,
});

provide("location", location);
provide("geolocation", geolocation);
},
};

添加响应 inject

在 setup() 中使用 inject 时,还需要从 vue 显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。

inject 函数有两个参数:

要注入的 property 的名称
一个默认的值 (可选)

1
2
3
4
5
6
7
8
9
10
11
export default {
setup() {
const userLocation = inject("location", "The Universe");
const userGeolocation = inject("geolocation");
console.log(userLocation, userGeolocation);
return {
userLocation,
userGeolocation,
};
},
};

修改响应式 property
当使用响应式提供/注入值时,建议尽可能,在提供者内保持响应式 property 的任何更改。

例如,在需要更改用户位置的情况下,我们最好在 使用 provide 祖先 组件中执行此操作。

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
// 祖先组件
export default {
components: {
About
},
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})
// 提供更新位置方法
const updateLocation = () => location.value = 'UpdateLocation'

provide('location', location);
provide('geolocation', geolocation);
provide('updateLocation', updateLocation)
}
};

// 后代组件
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const useUpdateLocation = inject('updateLocation') // 注入更改位置方法
console.log(userLocation, userGeolocation)
return {
userLocation,
userGeolocation,
useUpdateLocation
}
},
};

那么如何确保我们提供的属性不被后代组件所更改呢?建议对提供者的 property 使用 readonly。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { provide, ref, reactive, readonly } from "vue";
export default {
components: {
About,
},
setup() {
const location = ref("North Pole");
const geolocation = reactive({
longitude: 90,
latitude: 135,
});

const updateLocation = () => (location.value = "UpdateLocation");

provide("location", readonly(location));
provide("geolocation", readonly(geolocation));
provide("updateLocation", updateLocation);
},
};

强行赋值会报错 TypeError: “userLocation” is read-only

Fragments! 在 Vue 3 中,组件现在正式支持多根节点组件,即片段!

  • 2.x 语法
    在 2.x 中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个
1
2
3
4
5
6
7
8
<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
  • 3.x 语法
    在 3.x 中,组件现在可以有多个根节点!但是,这确实要求开发者明确定义属性应该分布在哪里。
1
2
3
4
5
6
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>

过渡的 class 名更改

非兼容 #概览
过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。

#2.x 语法
在 v2.1.8 版本之前, 为过渡指令提供了两个过渡类名对应初始和激活状态。

在 v2.1.8 版本中, 引入 v-enter-to 来定义 enter 或 leave 变换之间的过渡动画插帧, 为了向下兼容, 并没有变动 v-enter 类名:

.v-enter,
.v-leave-to {
opacity: 0;
}

.v-leave,
.v-enter-to {
opacity: 1;
}
这样做会带来很多困惑, 类似 enter 和 leave 含义过于宽泛并且没有遵循类名钩子的命名约定。

#3.x 语法
为了更加明确易读,我们现在将这些初始状态重命名为:

.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-leave-from,
.v-enter-to {
opacity: 1;
}
现在,这些状态之间的区别就清晰多了。

组件相关属性名也发生了变化:

leave-class 已经被重命名为 leave-from-class (在渲染函数或 JSX 中可以写为:leaveFromClass)
enter-class 已经被重命名为 enter-from-class (在渲染函数或 JSX 中可以写为:enterFromClass

Teleport

Vue 鼓励我们将 UI 和 UI 的行为封装到组件中,通过嵌套组件来构建我们的 App。但是存在这样的一种情景,有多个子组件从逻辑上看是属于同一个父组件的,但是从技术实现的角度来看,多个子组件可能应挂载在 DOM 的不同位置,比较常见的情景是 Modal。在 Vue3 之前,我们可以参考下 Element UI 中 Poper 的实现。

1
2
3
4
5
6
if (!popper || !reference) return;
if (this.visibleArrow) this.appendArrow(popper);
if (this.appendToBody) document.body.appendChild(this.popperElm);
if (this.popperJS && this.popperJS.destroy) {
this.popperJS.destroy();
}

我们可以发现,是通过 document.body.appendChild 方法将元素挂载到 body 上的。而在 Vue3 中我们可以通过 Teleport 来实现这一操作。img 将会挂载至 body 下。

1
2
3
4
5
6
<template>
<teleport to="body">
<img alt="Vue logo" src="./assets/logo.png?x-oss-process=image/auto-orient,1/quality,q_90/watermark,text_6auY5b2p6bmP,color_ef9191,size_30,shadow_100,x_15,y_15" />
</teleport>
<HelloWorld msg="Welcome to Your Vue.js App" />
</template>

Teleport 传送的元素依旧还会受 Vue 控制,这能很好的利用 Vue 的特性,可以说 Teleport 出现的大大增强了组件的可复用性和封装性。
最后多嘴一句,这里的名称或许叫 Portal 会让人更好理解,毕竟 V 社的传送门大家应该都玩过吧。好像是为了避免与可能出现的标签冲突才换了个动词。

Emits Component Option

验证自定义事件

如果自定义事件是通过对象语法声明(Object syntax)而不是数组语法声明(Array Syntax)的,那么这个自定义事件可以像 prop 校验一样完成校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.component("custom-form", {
emits: {
// No validation
click: null,

// Validate submit event
submit: ({ email, password }) => {
if (email && password) {
return true;
} else {
console.warn("Invalid submit event payload!");
return false;
}
},
},
methods: {
submitForm() {
this.$emit("submit", { email, password });
},
},
});

v-modal

当我们在自定义组件上使用 v-modal 时,默认的 prop 和事件发生变化,

prop:value->modalValue
event: input->update:modalValue

v-bind.sync以及modal参数被移除,并以 v-modal arguments 的形式替换。vue3 中 v-modal 语法糖的形式变更,在 vue3 中的 v-modal 等价形式变更为如下形式。

1
2
3
4
5
6
7
8
<ChildComponent v-model="pageTitle" />

<!-- would be shorthand for: -->

<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>

可以通过 v-modal arguments 改变 modal 的名字

1
2
3
4
5
<ChildComponent v-model:title="pageTitle" />

<!-- would be shorthand for: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

支持多个 v-modal 绑定

1
2
3
4
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>

支持创建自定义 v-modal 修饰符

1
<my-component v-model.capitalize="bar"></my-component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.component("my-component", {
props: {
modelValue: String,
modelModifiers: {
default: () => ({}),
},
},
template: `
<input type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
`,
created() {
console.log(this.modelModifiers); // { capitalize: true }
},
});

数据响应式变化的原理

vue2 使用 Object.defineProperty 把这些 property 全部转为 getter/setter。而 Vue2 在处理数组时,也会通过原型链劫持会改变数组内元素的方法,并在原型链观察新增的元素,以及派发更新通知

image

在 Vue3 中响应式系统最大的区别就是,数据模型是被代理的 JavaScript 对象了。不论是我们在组件的 data 选项中返回一个普通的 JavaScript 对象,还是使用 composition api 创建一个 reactive 对象,Vue3 都会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值等)。

具体原理慢慢研究。。。。。。。本次只是一些新 api 如何使用,具体怎么去使用达到最好的效果,还得看自己怎么去用吧