前言
Vue3已经发布快要1年半的时间了,目前很多企业已经开始着手将项目升级到Vue3了。甚至一些大厂已经在新项目中正式运用Vue3,说明他们已经将Vue3的生态环境搭建完成了。
那么,对于我们这些程序猿必然从此刻开始升级自己的技能包了。
动机
Vue3设计成组合API的方式去构建用户界面,本以为会是皆大欢喜、普天同庆的场面。但没想到在社区里确是褒贬不一。
这里说明一下官方的设计动机。那么Composition Api解决了什么问题呢?
-
逻辑关注点分离:在复杂组件中处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。这样碎片化的逻辑会让应用程序变得难以理解和维护,尤其是发生在新接手别人的项目时。
-
更便于逻辑封装与复用:基于hook函数的组合API,更大的发挥函数的优势,更容易去提取和封装复用的代码。
-
类型推断:Vue3对typescript更友好,使用ts开发时不用在像Vue2那样写那么多与业务无关的类型推断。
应用大法
如何优雅的使用Composition Api
1. setup
习惯了Vue2的写法,突然转到3版本还是有点不适应。比如,从哪里开始组合Composition呢?
新的 setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。
注意:在
setup中应该避免使用this,因为它不会找到组件实例。setup的调用发生在dataproperty、computedproperty 或methods被解析之前,所以它们无法在setup中被获取。
以单文件组件SFC为例:
<script setup>
// some statements
</script>
<template>
<h1>Hello, Vue3.</h1>
</template>
<style>
// some styles
</style>
<script setup>
- 每个
*.vue文件最多可同时包含一个<script setup>块 (不包括常规的<script>) - 该脚本会被预处理并作为组件的
setup()函数使用,也就是说它会在每个组件实例中执行。<script setup>的顶层绑定会自动暴露给模板。
<template>
- 每个
*.vue文件最多可同时包含一个顶层<template>块。 - 其中的内容会被提取出来并传递给
@vue/compiler-dom,预编译为 JavaScript 的渲染函数,并附属到导出的组件上作为其render选项。 - 简单来说就是指定组件的视图模板
<script>
- 每一个
*.vue文件最多可同时包含一个<script>块 (不包括<script setup>)。 - 该脚本将作为 ES Module 来执行。
- 其默认导出的内容应该是 Vue 组件选项对象,它要么是一个普通的对象,要么是 defineComponent 的返回值。
<style>
- 一个
*.vue文件可以包含多个<style>标签。 <style>标签可以通过scoped或moduleattribute (更多详情请查看 SFC 样式特性) 将样式封装在当前组件内。多个不同封装模式的<style>标签可以在同一个组件中混用。
1.1 顶层的绑定会被暴露给模板
当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用:
<script setup>
// 变量
const msg = 'Hello,Vue3!'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<div @click="log">{{ msg }}</div>
</template>
import 导入的内容也会以同样的方式暴露,并不需要通过 methods、components 选项来暴露它:
<script setup>
import { capitalize } from './helpers';
import HelloWorld from './components/HelloWorld.vue';
</script>
<template>
<div>{{ capitalize('hello') }}</div>
<HelloWorld />
</template>
这里也看到了:在Vue3中template块没有单根元素的限制了。
2. 响应式 vs data
我们知道在Vue2中data数据属性是响应式,那么组合Api中如何实现呢?
首先,要明确的是虽然在<script setup>中顶层声明的变量、函数等都可以在视图模板中直接引用,但是默认情况下是非响应式的。
<script setup>
let count = 0; // 非响应式
function addCount() {
// 虽然每次点击按钮这里会将count加1,但是不会引发视图更新
count++;
}
</script>
<template>
<button @click="addCount">u click me {{ count }} times.</button>
</template>
- 针对基本数据类型(比如数字、字符串、bool值)通过ref函数将其转变成响应式的。
<script setup>
import { ref } from 'vue';
let count = ref(0); // 响应式
function addCount() {
// 虽然每次点击按钮这里会将count.value加1,同时触发视图更新
count.value++;
}
</script>
<template>
<!-- 视图模板中count自动解包,即自动获取count的value属性值 -->
<button @click="addCount">u click me {{ count }} times.</button>
</template>
ref函数接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。
- 而针对对象、数组这种复杂数据类型可以通过reactive将其转变成响应式的。
<script setup>
import { reactive } from 'vue';
import AddTodoVue from './AddTodo.vue';
import TodoItem from './TodoItem.vue';
let todos = reactive([]); // 响应式
// add todo
function addTodo(name) {
// 当todos添加新的todo时,视图会同步更新
todos.push({ id: Date.now(), name, isFinished: false });
}
</script>
<template>
<div>
<h1>Todos</h1>
<AddTodoVue @add="addTodo" />
<ul>
<TodoItem v-for="todo of todos" :key="todo.id" :todo="todo" />
</ul>
</div>
</template>
reactive函数返回对象的响应式副本。响应式转换是“深层”的——它影响所有嵌套 property。
3. 样式绑定 & v-bind
Vue3中依旧可以像Vue2中那样动态绑定class和style。除此之外,<script setup>块中的变量也可以通过v-bind注入到style块中.
<script setup>
import { reactive } from 'vue';
const styles = reactive({
color: 'hotpink',
fontSize: '20px',
});
</script>
<template>
<div class="text-custom">my name is guojing.</div>
</template>
<style>
.text-custom {
// 通过v-bind注入
color: v-bind('styles.color');
font-size: v-bind('styles.fontSize');
}
</style>
4. 计算属性
computed函数接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式对象。
<script setup>
import { reactive } from 'vue';
import AddTodoVue from './AddTodo.vue';
import TodoItem from './TodoItem.vue';
let todos = reactive([]); // 响应式
// add todo
function addTodo(name) {
// 当todos添加新的todo时,视图会同步更新
todos.push({ id: Date.now(), name, isFinished: false });
// 计算属性
let unfinishedTodos = computed(() => todos.filter((todo) => !todo.isFinished));
}
</script>
5. 监听属性
watch API 与选项式 API 完全等效。watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
侦听单一源
侦听器数据源可以是一个具有返回值的 getter 函数,也可以直接是一个 ref:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
侦听多个源
侦听器还可以使用数组以同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
6. 生命周期
通过Vue2中生命周期钩子前加上“on”之后,即可在setup中组合使用了。
| 选项式 API | Hook inside setup |
|---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
TIP
因为
setup是围绕beforeCreate和created生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写。
7. 双向绑定v-model
在处理表单元素时,vue3与2版本无差异。但是当作用在自定义组件上时,区别还是很大的。
当用在组件上时,v-model 则会这样:
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
相当于给组件绑定属性“modelValue”以及注册一个“update:modelValue”事件。
CustomInput组件
<script setup>
import { computed } from 'vue';
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
let value = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
</script>
<template>
<input type="text" v-model="value" />
</template>
父组件
<script setup>
import CustomInput from './components/CustomInput.vue';
import { ref } from 'vue';
let text = ref('');
</script>
<template>
<p>{{ text }}</p>
<CustomInput v-model="text" />
</template>
本质上,v-model指令的语法是这样的:
v-model:<选项>="表达式"
那么我们就可以在组件上多次使用v-model指令。
子组件
<script setup>
defineProps(['name', 'age']);
const emit = defineEmits(['update:name', 'update:age']);
function changeUser() {
emit('update:name', 'guojing');
emit('update:age', 19);
}
</script>
<template>
<div @click="changeUser">我叫{{ name }}, 今年{{ age }}岁了。</div>
</template>
父组件
<script setup>
import { reactive } from 'vue';
import Child from './components/Child.vue';
let user = reactive({
name: 'guoguo',
age: 18,
});
</script>
<template>
<Child v-model:name="user.name" v-model:age="user.age" />
</template>
8. await支持
<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup():
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
总结
Vue3中组合API还可以配置vue-router以及vuex(Pinia)等,这里就先不介绍了。
大家可以在评论区留言,互相探讨Vue3带来一些惊喜。