前言
众所周知,Vue.js@3 新增了 setup
语法,让开发者在编写组件时更加简洁直接。表面上看,setup
语法糖只是一种语法上的优化,但它背后到底做了什么?是否与传统的 setup
函数写法完全一致?让我们通过一些分析来了解它的本质。
简单示例
首先来看一下使用 setup
语法糖的写法:
<script setup>
const msg = "Hello, world";
const count = ref(0);
function increase() {
count.value++;
}
</script>
这种写法相当于:
export default {
setup() {
const msg = "Hello, world";
const count = ref(0);
function increase() {
count.value++;
}
return { msg, count, increase };
},
};
表面上看,这两种语法在效果上是相同的,都是在组件中定义变量、响应式数据和方法,并且将这些东西暴露出来供模板使用。但它们真的完全一致吗?我们通过一些对比来深入了解它们之间的差异。
深入分析
为了更清楚地看到两种写法的差异,我们创建了两个组件:一个使用传统的 setup
函数(FooSetup
),另一个使用 script setup
语法糖(FooScriptSetup
)。
<template>
<FooSetup ref="refSetup" />
<FooScriptSetup ref="refScriptSetup" />
</template>
<script setup>
import FooSetup from "./components/FooSetup.vue";
import FooScriptSetup from "./components/FooScriptSetup.vue";
import { onMounted, ref } from "vue";
const refSetup = ref(null);
const refScriptSetup = ref(null);
onMounted(() => {
console.log(refSetup.value);
console.log(refScriptSetup.value);
});
</script>
接下来,我们去控制台打印出数据。
在控制台打印出这两个组件的实例后,发现使用传统写法的组件实例中暴露了很多成员,而使用 setup
语法糖的组件实例却没有暴露任何东西。为什么会出现这样的差异?
而使用语法糖打印出来的结果里面却什么都没有。为什么会造成这种差异呢?
编译差异
- 使用插件来查看单文件组件的编译结果
传统写法和使用语法糖之后编译出来的东西是不一样的,因为最终运行的一定不是单文件组件,而是单文件组件编译出来的结果,这个时候,我们需要借助一个插件 vite-plugin-inspect
,启动 Vite.js 之后,它会生成两个地址:第二个地址就是查看编译结果的地址。
import Insepct from 'vite-plugin-inspect'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
Insepct()
],
})
可以看到工程里边编译了几个文件,我们把重点聚焦在 FooScript
和 FooScriptSetup
,看它们两个的编译结果。
- FooScript 传统写法的组件的编译结果
首先来看 FooScript
这个使用传统写法的组件它的编译结果。我们可以清晰地看到,template
语法在编译结果后不存在了,而是被转为了一个 render
函数,在 template
里的什么乱七八糟的指令、属性绑定、v-for、v-if 等这些东西,最终都不复存在,全部变成了 render
函数里边。
接着,我们再重点关注 setup
函数,我们可以看到是,它原封不动地被编译出来。写的代码是啥,最终的编译结果就是啥。
- FooScriptSetup 语法糖写法的组件的编译结果
首先可以看到,template
同样会被编译成 render
函数,这个没区别。
但是 <script setup>
语法糖里的内容的编译结果,似乎与传统写法的组件的编译结果相比,并没有太多差异。变量、响应式数据、函数都被 return
出来。
但是!仔细观察,右边编译结果多了一个不起眼的 expose
。
那为什么多了这条语句之后造成了代码编写上的差异?
不起眼的 Expose
在 Vue.js 官方文档里,有专门对它的描述。expose
作为一个数组属性,有两种用法:
- 一种是在传统写法里配置,它表示我要向外界暴露出这个组件实例中有哪些成员?
expose: [
'a',
],
methods: {
a() { }
},
setup() {
const msg = 'Hello, world'
const count = ref(0)
function increase() {
count.value++
}
return {
msg, count, increase
}
}
我们这里在 methods
属性中添加了一个 a
方法,如果说我们希望父组件能够拿到这个组件的 a
方法,那么就在 expose
数组中使用字符串来添加该函数的名字。
一旦我们在实例中使用了 expose
方法,那么控制台就不会将所有组件实例拥有的属性、方法都给暴露出来,而只暴露出我们 expose
数组里指定的东西。
可以看到,控制台打印出来的传统写法的组件,已经与使用语法糖写法的组件没有区别了。看到这里,我想正在阅读这篇文章的你,已经有些眉目了。大概了解了语法糖它本质是怎样的 😂。
- 第二种写法
我们还可以在 setup
函数里将 expose
给结构出来。
setup(props, { expose }) {
const msg = 'Hello, world'
const count = ref(0)
function increase() {
count.value++
}
expose({
msg
})
return {
msg, count, increase
}
}
可以看到,控制台打印出了这个组件实例暴露出来的 msg
。
如果我们只调用 expose
,但是也没有传参,表示我们不希望暴露出任何东西,那么这样子一来,控制台就不会打印出组件实例上任何东西了。
所以说,如果我们使用了 setup
语法糖,那么 Vue.js 会自动给你加上 expose()
,让组件实例不暴露任何东西。
为什么要多此一举
Vue.js@3 为什么要这样子做?把组件实例里有的东西都暴露出来不好吗?
当然不是,这实际上不是一件好事。因为这种做法就提供了一种可能,你在这个组件实例之外,你可以通过 reference
去拿到这个组件实例上的任何成员,比方说,我给 FooScript
加个 reference
,然后父组件通过 ref.count
拿到子组件的响应式数据,这就打破了单向数据流的约定。
<script>
onMounted(() => {
console.log(refFooScript.value);
// 修改子组件的内部数据
refFooScript.value.count = 2;
console.log(refFooScript.value.count);
console.log(refFooScript.value);
});
</script>
count
是组件内部的数据,你就给外部提供了一种改动它的可能性, 这种做法存在很大的隐患。单项数据流这种约定一旦被打破,代码就离屎山越来越近。
所以,为了解决这个问题,vue.js@3就引入了 expose
方法,而在 setup
语法糖里,Vue.js@3在编译过程中自动帮我们引入 。
一定要暴露?
那如果我们一定要暴露子组件某个响应式数据或方法呢?
<script setup>
import { ref } from 'vue';
const msg = 'Hello, world'
const count = ref(0)
function increase() {
count.value++
}
defineExpose({
msg,
count,
increase
})
</script>
如果使用的是 setup
语法糖的话,直接使用 defineExpose
就好。当然,defineExpose
是一个宏,它并不参与运行时,它只是参与编译,类似的还有 defineProps, defineEmits
,这些都是不参与运行。
总结
通过以上分析,我们可以看出,setup
语法糖不仅仅是语法上的简化,更是在编译和实例暴露行为上的精细控制,理解这些差异有助于我们在实际开发中更好地使用 Vue.js 3,编写出更安全、可维护的代码。 避免屎山。