Vue3 的组件可以有两种不同的风格书写,它们包括 选项式 API (Options API) 和 组合式 API (Composition API)。

选项式 API 还是和之前版本的 Vue 一样,使用 export 导出一个包含各种选项的对象,里面可以包含 data 数据、methods 方法、components 组件注册。数据和方法也会直接暴露到 this ,可以直接通过 this 来使用数据和方法。

组合式 API 不需要像选项式 API 一样把所有的数据和方法都写在一个对象里,组合式 API 直接使用变量和函数,数据可以放到 let 变量和 const 常量里,方法可以直接定义函数,写法比较接近原生 JS。

这篇文章主要是写组合式 API script setup 的基本使用,包括 响应式、生命周期函数、事件、自定义指令、Vue Router 路由、Vuex 状态管理的写法,因为主要式以展示写法为主,所以对每个功能不会太深入。

组合式 API 的写法

组合式 API 需要和 setup 搭配使用,它的写法有两种,一种是和选项式 API 一样使用 export 导出一个对象,把变量之类放到 setup 方法里,然后返回。另一种就是直接在 script 标签添加 setup 属性,然后直接在 script 内写变量和函数,不需要使用 export 导出对象。

使用对象的写法如下:

<template>
  <div>{{ text }}</div>
</template>

<script>
import {ref} from 'vue';

export default {
  setup() {
    const text = ref('Hello Vue3');
    
    return {
      text
    };
  },
  created() {
    console.log(this.text.value);
  }
}
</script>

变量之类的需要写在 setup 里,通过 return 返回后可以直接通过 this 访问到。

setup 会在 beforeCreate 之前执行,在 beforeCreate 中可以通过 this 访问 setup 返回的内容。

直接给 script 添加 setup 属性的写法如下:

<template>
  <div>{{ text }}</div>
</template>

<script setup>
import {ref, onBeforeMount} from 'vue';

let text = ref('Hello Vue3');

// 组件元素被挂载之前执行
onBeforeMount(() => {
  console.log(text.value);
});
</script>

setup 内可用的钩子函数和选项式 API 的有些不一样,如果你使用的是对象的写法的话,建议只用一种钩子函数。如果是 setup 属性的写法的话,是不支持之前的钩子函数的。

Vue3 的 setup 语法糖就是直接在 script 标签加 setup 属性,我下面的内容也是直接使用 setup 属性。

组件引入和注册

之前的对象写法使用 import 引入组件后需要在 components 中注册组件才能使用,使用 setup 写法引入组件后就可以直接在 template 内使用,无需注册。

下面引入组件:

<template>
  <div>
    <menuList />
  </div>
</template>

<script setup>
import menuList from './menu-list.vue';
</script>

响应式

在组合式对象写法中 data 里的数据都是响应式的,当 data 里的数据发生改变时,页面也能及时响应变化。

setup 中,如果直接把变量绑定到 template 的话,变量的值发生改变时,页面也不会响应变化。

下面把变量直接绑定到页面:

<template>
  <div>
    <button type="button" @click="count ++">{{ count }}</button>
  </div>
</template>

<script setup>
let count = 0;
</script>

上面的按钮绑定了 count ,点击按钮后 count 会 +1,但是页面上显示的内容并不会发生改变。如果使用 console.log 查看 count ,可以发现 count 的内容是发生了改变的,但是页面并不会响应变化。

如果需要让页面响应变化,可以使用 refreactive

ref

ref 可以接收一个值,返回一个可更改的响应式对象。

下面把 ref 返回的对象绑定到页面:

<template>
  <div>
    <button type="button" @click="count ++">{{ count }}</button>
  </div>
</template>

<script setup>
import {ref} from 'vue';
let count = ref(0);
</script>

ref 返回的是一个对象,对象里包含一个 valuevalue 就是 ref 传入的值,如果直接在页面模板绑定 ref 对象也能直接获取 value 值,但如果要在 script 中使用就需要访问 value

import {ref} from 'vue';
let count = ref(0);
// 在控制台输出 count 的 value
console.log(count.value);

ref 也可以传入对象:

<template>
  <div>{{ user.name }}</div>
</template>

<script setup>
import {ref} from 'vue';
const user = ref({
  name: '小张',
  age: 12
});
console.log(user.value.name);
console.log(user.value.age);
</script>

reactive

reactiveref 的使用是差不多的,可以传入一个对象,返回一个响应式的对象。reactive 只能传入对象、数组、Map ,返回的对象也不需要通过 value 调用。

<template>
  <div>{{ user.name }}</div>
</template>

<script setup>
import {reactive} from 'vue';
const user = reactive({
  name: '小张',
  age: 12
});
console.log(user.name);
console.log(user.age);
</script>

如果需要创建响应式对象的话,使用 reactive 性能会更好一些。

生命周期钩子

setup 中也提供了一些生命周期钩子 API,这些 API 会在组件挂载前后、组件更新前后、组件销毁前后被调用。

每一个函数在调用之前都需要使用 import 引入,下面是常用的钩子函数:

<template>
  <div>
    <button id="btn" @click="btnName = '博客'">{{ btnName }}</button>
  </div>
</template>

<script setup>
import {
  onMounted,
  onUpdated,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount,
  onUnmounted,
  ref
} from 'vue';

const btnName = ref('misterma.com');

// 组件渲染之前(此时还无法操作 DOM 元素)
onBeforeMount(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出
  console.log(document.querySelector('#btn'));  // 输出为 null
});

// 组件渲染完成后(已经可以操作 DOM 元素)
onMounted(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出元素的 HTML
  console.log(document.querySelector('#btn').innerHTML);  // 输出为 misterma.com
});

// 组件元素即将更新前(元素还没有发生改变)
onBeforeUpdate(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出元素的 HTML
  console.log(document.querySelector('#btn').innerHTML);  // 输出为 misterma.com
});

// 组件元素更新完成后(元素已发生改变)
onUpdated(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出元素的 HTML
  console.log(document.querySelector('#btn').innerHTML);  // 输出为 博客
});

// 组件即将被销毁前
onBeforeUnmount(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出元素的 HTML
  console.log(document.querySelector('#btn').innerHTML);  // 可以正常输出元素 html
});

// 组件被销毁后(此时已经无法操作 DOM 元素)
onUnmounted(() => {
  // 获取 id 为 btn 的元素,然后在控制台输出
  console.log(document.querySelector('#btn'));  // 输出为 null
});
</script>

上面只是一些常用的钩子函数,Vue 还提供了一些用于开发调试的函数,完整的函数说明可以看 Vue 官方文档 - 生命周期钩子 API 索引

计算属性 computed

template 模板中可以做一些简单的条件判断和计算,但是考虑到可读性的原因,复杂一些的就可以考虑放到 computed 判断和计算。

下面再 template 模板中判断:

<template>
  <div>是否成年:{{ user.age >= 18 ? '已成年' : '未成年' }}</div>
</template>

<script setup>
import {reactive} from 'vue';
const user = reactive({
  name: '小张',
  age: 12
});
</script>

上面会根据 userage 来判断输出 成年未成年

上面的写法可读性就不太好,如果要在多个地方输出的话,也需要判断多次。

下面使用 computed 来判断输出:

<template>
  <div>是否成年:{{ adult }}</div>
</template>

<script setup>
import {reactive, computed} from 'vue';
const user = reactive({
  name: '小张',
  age: 19
});

const adult = computed(() => {
  // 根据 user 的 age 返回成年或未成年
  return user.age >= 18 ? '已成年' : '未成年';
});
</script>

如果上面的 user.age 发生改变 computed 也能响应变化。

事件处理

template 模板内还是和之前的选项式 API 一样的使用 v-on:事件名称@事件名称 来监听事件。

setup 的事件方法可以直接定义函数,不需要像选项式 API 一样的写在 methods 里。

<template>
  <div>
    <button type="button" @click="showTag">按钮</button>
  </div>
</template>

<script setup>
function showTag(event) {
  // 显示标签名称
  alert(event.target.tagName);
}
</script>

模板事件调用函数的时候也可以在括号里传参,事件函数如果需要同时接收参数和事件 event ,在传参的时候可以传入一个 $event ,如下:

<template>
  <div>
    <button type="button" @click="showAlert('Hello', $event)">按钮</button>
  </div>
</template>

<script setup>
function showAlert(message, event) {
  // 显示标签名称
  alert(event.target.tagName);
  // 显示传入的 message
  alert(message);  // 输出了 hello
}
</script>

defineEmits 声明触发事件

在之前的选项式写法中可以使用 this.$emit 来给父组件传值和触发父组件的自定义事件,在 script setup 中就不能直接使用 this 来调用了。

script setup 中提供了一个 defineEmits 来注册和调用父组件的事件,用法如下:

<template>
  <div>
    <button type="button" @click="buttonClick">按钮</button>
  </div>
</template>

<script setup>
import {defineEmits} from 'vue';
// 事件名称,可以通过数组传入多个事件
const emit = defineEmits(['message']);

function buttonClick() {
  // 调用上面注册的 message 事件
  emit('message', '这是从 content 组件传来的内容');
}
</script>

父组件:

<template>
  <!--contentPage子组件-->
  <contentPage @message="showMessage" />
</template>

<script setup>
import contentPage from './components/contentPage.vue';

function showMessage(text) {
  // 使用 alert 输出子组件传来的内容
  alert(text);
}
</script>

调用 defineEmits 的时候可以传入一个包含要触发的事件的数组,返回一个函数,需要触发事件的时候可以直接调用返回的函数,传入事件名称。

defineEmits 需要直接在 script setup 下调用,不能在子函数中调用。

defineExpose API

在选项写法中通过 ref 注册引用,父组件可以使用 this.$refs 来获取和操作子组件的数据和方法,但是在 script setup 中,父组件式不能访问子组件的变量和方法的。

script setup 写法中,使用 defineExpose 可以把指定的变量和方法暴露出去,父组件通过 ref 注册后可以获取和操作子组件暴露的变量和方法。

子组件:

<template>
  <div>
    <p>{{ text }}</p>
    <p>{{ num }}</p>
  </div>
</template>

<script setup>
import {defineExpose, ref} from 'vue';

const text = ref('我的博客 www.misterma.com');
const num = ref(1);

defineExpose({text, num});
</script>

父组件:

<template>
  <!--contentPage子组件-->
  <contentPage ref="dataSet" />
  <button type="button" @click="change">按钮</button>
</template>

<script setup>
import contentPage from './components/contentPage.vue';
import {ref} from 'vue';

const dataSet = ref(null);

function change() {
  // 在控制台输出子组件暴露出的 text 和 num
  console.log(dataSet.value.text);
  console.log(dataSet.value.num);
  // 更改子组件暴露出的 text 和 num
  dataSet.value.text = 'Github https://github.com/changbin1997';
  dataSet.value.num ++;
}
</script>

上面的子组件中暴露出的是 ref 响应式对象,父组件也需要通过 value 来操作,父组件更改数据后子组件也能立即响应变化。

watch 侦听器

在对象写法的选项式 API 中 watch 相关的方法只需要放在 watch 对象中就可以使用。在组合式 API 中需要引入 watch 模块,传入监听变量和回调函数使用。

<template>
  <div>
    <button type="button" @click="count ++">点击次数:{{ count }}</button>
  </div>
</template>

<script setup>
import {watch, ref} from 'vue';
const count = ref(0);

watch(count, () => {
  // count 发生改变时在控制台输出 count.value
  console.log(count.value);
});
</script>

watch 的回调函数可以接收两个参数,第一个是变化之后的值,第二个是变化之前的值:

<template>
  <div>
    <button type="button" @click="count ++">点击次数:{{ count }}</button>
  </div>
</template>

<script setup>
import {watch, ref} from 'vue';
const count = ref(0);

watch(count, (newCount, previous) => {
  console.log(newCount);  // 输出变化后的值
  console.log(previous);  // 输出变化前的值
});
</script>

ref 模板引用

Vue 虽然可以通过绑定数据的方式来操作 DOM,但是有的功能通过绑定数据还是无法实现,需要直接操作底层 DOM 元素。

在前面的响应式中通过引入 ref 可以创建一个响应式对象,这里的模板引用也需要用到一个 ref 属性。

下面实现组件加载完成后让指定元素获取焦点:

<template>
  <div>
    <input type="text" ref="inputEl">
  </div>
</template>

<script setup>
import {ref, onMounted} from 'vue';

const inputEl = ref(null);  // 用来存放元素
// 组件加载完成后
onMounted(() => {
  // 让 input 获取焦点
  inputEl.value.focus();
});
</script>

注意,操作 DOM 元素需要等元素被插入到页面后才能操作,组件需要加载到 onMounted 阶段才能操作,如果页面元素因为数据发生了改变,需要到 onUpdated 阶段才能操作改变后的元素!

自定义指令

在 Vue 的模板中有一种以 v- 开头的属性,这就是指令。Vue 内置了一些指令,比如 v-forv-model

除了使用内置指令外,也可以自定义指令,自定义指令主要用来操作 DOM 元素。相比上面的使用 ref 来操作元素,自定义指令可以全局注册,全局注册后在每个组件都可以直接使用。

局部使用

自定义指令可以全局注册,也可以局部使用,局部使用就是把指令写在单独的组件里。

在选项式 API 的对象写法中,自定义指令需要写在 directives 里,在选项式 setup 中,自定义指令需要创建单独的对象,对象名就是指令名。

下面实现组件元素加载完成后让指定元素获取焦点:

<template>
  <div>
    <input type="text" v-focus>
  </div>
</template>

<script setup>
const vFocus = {
  mounted: el => {
    el.focus();
  }
};
</script>

自定义指令的名称需要以 v 开头,使用驼峰命名,模板中调用的时候以 v- 开头。

全局注册

全局注册需要在加载根组件之前,可以放到 main.js ,也可以写在单独的 JS 文件,然后在 main.js 中引入。

下面还是实现页面加载完成后让指定元素获取焦点,这里我为了方便就直接放到 main.js

import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.directive('focus', {
  mounted: (el) => {
    el.focus();
  },
});

app.mount('#app');

在每个组件都能调用:

<template>
  <div>
    <input type="text" v-focus >
  </div>
</template>

Props

props 可以用于组件间传递数据,父组件如果要给子组件传递数据就可以使用 props。

在选项式的对象写法中 props 可以直接在 props 选项中定义字符串数组,在 setup 组合式写法中 props 需要引入 defineProps ,在 defineProps 中可以传入一个字符串数组。

<template>
  <div>
    <p>{{ firstName }}</p>
    <p>{{ lastName }}</p>
  </div>
</template>

<script setup>
import {defineProps} from 'vue';

defineProps(['firstName', 'lastName']);
</script>

如果需要在 script 中访问 props 可以创建一个变量来接收 defineProps

父组件还是一样的在 template 模板中使用 props 的数组的字符串值作为属性来传递数据,例如我父组件要给上面子组件的 firstNamelastName 传值,我可以这样写:

<template>
  <contentPage firstName="LeBron" lastName="James" />
</template>

Vue Router 路由

在选项式写法中可以通过 this.$routerthis.$route 来操作路由跳转和访问路由参数,在 script setupthis 是访问不到路由的。

main.js 简单配置:

import { createApp } from 'vue';
import {createRouter, createWebHashHistory} from 'vue-router';
import App from './App.vue';
// 引入两个用于路由跳转的页面组件
import homePage from './components/homePage.vue';
import contentPage from './components/contentPage.vue';

const app = createApp(App);
// 创建和配置路由
const router = createRouter({
  routes: [
    {path: '/', component: homePage, name: 'homePage'},
    {path: '/content', component: contentPage, name: 'contentPage'}
  ],
  history: createWebHashHistory()
});
// 注册路由
app.use(router);

app.mount('#app');

上面只是简单演示,所以路由配置就直接写在 main.js 里了。

下面我要在点击按钮后使用 push 跳转到 contentPage 页面:

<template>
  <div>
    <button type="button" @click="buttonClick">跳转到 Content Page</button>
  </div>
</template>

<script setup>
import {useRouter} from 'vue-router';

const router = useRouter();

function buttonClick() {
  router.push({
    name: 'contentPage',
    query: {id: 1,page: 1}
  });
}
</script>

上面在跳转到 contentPage 页面的时候还传了 idpage 参数,下面就在 contentPage 页面中获取参数:

<script setup>
import {useRouter} from 'vue-router';

const router = useRouter();
// 在控制台输出 url 参数
console.log(router.currentRoute.value.query.id);
console.log(router.currentRoute.value.query.page);
</script>

Vuex 状态管理

目前用于 Vue3 的状态管理库官方推荐的是 Pinia,但 Vuex 也可以在 Vue3 使用。如果你还没有学习过 Pinia 或是更习惯用 Vuex ,也可以继续在 Vue3 中使用 Vuex。

script setup 中不能使用 this.$store ,这里简单写一下在 script setup 组件中获取和更改 Vuex 状态的方法。

main.js 配置:

import { createApp } from 'vue';
import {createStore} from 'vuex';
import App from './App.vue';

const app = createApp(App);

// 配置 Vuex
const store = createStore({
  state() {
    return {
      text: '我的博客 www.misterma.com',
      num: 12
    }
  },
  mutations: {
    changeText(state) {
      state.text = 'github https://github.com/changbin1997';
    },
    changeNum(state) {
      state.num ++;
    }
  }
});

app.use(store);
app.mount('#app');

这里只是简单演示,所以 Vuex 配置就不拆分为单独的文件了,还是放在 main.js 里。

下面在组件中获取和设置 Vuex 的状态:

<template>
  <div>
    <!--输出 store 配置的 text 和 num-->
    <p>{{ store.state.text }}</p>
    <p>{{ store.state.num }}</p>
    <button type="button" @click="buttonClick">更改 text 和 num</button>
  </div>
</template>

<script setup>
import {useStore} from 'vuex';

const store = useStore();

// 在控制台输出 store 配置的 text 和 num
console.log(store.state.text);
console.log(store.state.num);

function buttonClick() {
  // 更改 text 和 num
  store.commit('changeText');
  store.commit('changeNum');
}
</script>

以上就是 script setup 各种 API 的写法,这里只是以展示写法为主,对于每个 API 不会太深入。