Vue3 的 setup 语法糖
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
的内容是发生了改变的,但是页面并不会响应变化。
如果需要让页面响应变化,可以使用 ref
或 reactive
。
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
返回的是一个对象,对象里包含一个 value
,value
就是 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
reactive
和 ref
的使用是差不多的,可以传入一个对象,返回一个响应式的对象。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>
上面会根据 user
的 age
来判断输出 成年
或 未成年
。
上面的写法可读性就不太好,如果要在多个地方输出的话,也需要判断多次。
下面使用 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-for
、v-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 的数组的字符串值作为属性来传递数据,例如我父组件要给上面子组件的 firstName
和 lastName
传值,我可以这样写:
<template>
<contentPage firstName="LeBron" lastName="James" />
</template>
Vue Router 路由
在选项式写法中可以通过 this.$router
和 this.$route
来操作路由跳转和访问路由参数,在 script setup
中 this
是访问不到路由的。
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
页面的时候还传了 id
和 page
参数,下面就在 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 不会太深入。
版权声明:本文为原创文章,版权归 Mr. Ma's Blog 所有,转载请联系博主获得授权。
本文地址:https://www.misterma.com/archives/921/
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。