Vue3葵花宝典:组合API的应用大法

197 阅读3分钟

前言

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 的调用发生在 data property、computed property 或 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> 标签可以通过 scopedmodule attribute (更多详情请查看 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 导入的内容也会以同样的方式暴露,并不需要通过 methodscomponents 选项来暴露它:

<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>
  1. 针对基本数据类型(比如数字、字符串、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,指向该内部值。

  1. 而针对对象、数组这种复杂数据类型可以通过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中组合使用了。

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

TIP

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 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带来一些惊喜。