写在开头
作为一名 Vue 的骨灰级玩家(这里特别感谢一下尤大大祖师爷赏饭吃😋),自从 Vue3 横空出世,其革命性的 Composition API 就给我们带来了前所未有的开发体验。
小编使用 Vue3 也有挺长一段时光了,然而,在 Vue3 的应用中,俺有时候发现团队项目中会发现存在 setup()
函数与 script setup
语法混合使用的情况;这个单文件(SFC)用一个形式,另一个单文件又换一种形式😬。初看之下,它们似乎只是在语法层面上有所差异,但并不会影响具体的功能逻辑。
而,真是这样吗?相信在大多数人的印象里 script setup
仅是一个更加便捷的写法,并没有其他特别作用。但事实上,它们之间的区别远不止于此。
下面,小编将分享两者更加细微的一些区别,咱从实战与源码的角度,通过两个功能相同的组件来展示它们的不同表现情况,Go!
课前准备
先来做点准备,初始化一个 Vue3 项目,这过程...(此处省略xxx),再搞两个功能相同的组件耍耍。😁
新建 FunctionSetup.vue
文件:
<template>
<h1>{{ message }}</h1>
<h2>count:{{ count }}</h2>
<button @click="handleClick">点击</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const message = ref('我是script setup形式');
const count = ref(0);
function handleClick() {
count.value++;
}
</script>
新建 FunctionSetup.vue
文件:
<template>
<h1>{{ message }}</h1>
<h2>count:{{ count }}</h2>
<button @click="handleClick">点击</button>
</template>
<script lang="ts">
import { ref } from 'vue';
export default {
setup() {
const message = ref('我是setup()形式');
const count = ref(0);
function handleClick() {
count.value++;
}
return {
message, count, handleClick
};
}
};
</script>
发现问题
在 App.vue
文件引入使用:
<template>
<FunctionSetup ref="functionSetup" />
<ScriptSetup ref="scriptSetup" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import FunctionSetup from './components/FunctionSetup.vue';
import ScriptSetup from './components/ScriptSetup.vue';
const functionSetup = ref(null);
const scriptSetup = ref(null);
onMounted(() => {
console.log('functionSetup', functionSetup.value)
console.log('scriptSetup', scriptSetup.value)
})
</script>
由于两个组件功能是一样的,所以在页面上表现也是相同的,这没什么好说的;而语法上,虽然有差异,但这还不是我们观察的重点,那我们就只能来看看它们俩的实例上有啥区别了。
通过 ref
咱们获取到两个组件的实例,并在 onMounted
钩子中将它们打印了出来,经过小编的一阵翻找,确实有点差异存在, 如下:
可以看到通过 setup()
函数编写的组件,组件实例会自动暴露组件的公共方法和属性,而通过 script setup
形式编写的组件却是不会自动暴露。😯
在以前 Vue2 的项目中,你可能有写过如下的代码:
<template>
<div>
<button @click="add">添加用户</button>
<!-- ... -->
<add-user-dialog ref="addUserDialog" />
</div>
</template>
<script>
export default {
methods: {
add() {
this.$refs.addUserDialog.open();
}
}
}
</script>
以上代码在 Vue2 中是一种较为常用的方式,使用起来非常简单方便,即能够通过组件实例直接调用组件内部的方法。在此,咱们暂且不考量其好坏与否,而来思考一下将其迁移到 Vue3 中是否还能正常使用。
以调用上方两个组件为示例:
<template>
<FunctionSetup ref="functionSetup" />
<ScriptSetup ref="scriptSetup" />
<button @click="clickHandle">测试组件的调用</button>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import FunctionSetup from './components/FunctionSetup.vue';
import ScriptSetup from './components/ScriptSetup.vue';
const functionSetup = ref(null);
const scriptSetup = ref(null);
onMounted(() => {
console.log('functionSetup', functionSetup.value)
console.log('scriptSetup', scriptSetup.value)
})
function clickHandle() {
// 先不管TS的问题
(functionSetup.value as any).handleClick();
(scriptSetup.value as any).handleClick(); // ❌
}
</script>
你会发现通过 setup()
函数编写的组件,父组件是能正常通过组件实例调用其他内部方法的,而以 script setup
形式编写的组件却无法被调用,这个结局也是意料之中的事。
从源码角度看问题
至此,咱们得到了一个 "结论",只要记住这个结论就行了吧,So easy。😊
而且,既然 script setup
是如此设计的,那必然也存在解决办法。Em......当然,这点我们下面再讨论。
这里我们再回过头来说,难道你就不好奇,Vue3 后来出现的 script setup
是怎样做到这一点的吗?它是如何做到不把组件内部的东西暴露出去的呢❓
好吧,反正小编是挺好奇的(不然怎么继续讲下去😋假装我很好奇就是),接下来我们就从源码的角度来探究这个问题。
给大家推荐一个插件: vite-plugin-inspect。
它能帮我们直接看穿 Vue 在背后到底做了些什么事情,这可真是一个神器啊,五星推荐必备。(⭐⭐⭐⭐⭐)
安装插件:
npm i -D vite-plugin-inspect
在 vite.config.ts
配置文件中引入插件:
// ...
import Inspect from 'vite-plugin-inspect';
export default {
plugins: [
// ...
Inspect()
],
}
重启一下服务,然后访问 http://localhost:5173/__inspect/#/
,你会看到如下页面:
咱们可以点击想查看文件,它会给我们展示 Vue 文件编译后的情况。
如以 setup()
函数编写的组件情况:
能看到 template
模版直接被编译成一个 render
函数了,当然,这不是我们关注的重点,略过略过。来看 script
部分,大体还是与我们原来写的逻辑是一致的。
再来瞧瞧,以 script setup
形式编写的组件情况:
template
模块部分还是一样,主要是 script
部分,可以看到最终还是套上了 setup()
函数,所以,咱们经常说 script setup
就是一个语法糖,这下就有了最有力的铁证。😀
然后,看看蓝色的框框,这是 script setup
形式与 setup()
函数的主要区别了❗
__isScriptSetup:
- 在 Vue3 中,
__isScriptSetup
是一个内部标记,主要用于识别组件是否是通过<script setup>
语法来定义的。这个标记对于 Vue 的编译器和运行时来说非常重要。 - 当 Vue 编译器处理组件时,它会根据这个标记来应用特殊的编译规则。例如,对于使用
<script setup>
的组件,变量和函数的暴露方式与传统的setup
函数不同。在<script setup>
中,定义的变量和函数会自动在模板中可用,而不需要像传统setup
函数那样显式地返回一个对象来暴露它们。__isScriptSetup
标记帮助 Vue 识别这种特殊的组件定义方式,从而正确地处理组件的变量和函数的暴露,以及其他相关的编译步骤。
__expose:
__expose
在 Vue3 中用于控制组件内部内容的暴露。在<script setup>
组件中,可以使用defineExpose
来指定要暴露给外部的属性和方法。虽然__isScriptSetup
和__expose
有不同的功能,但它们在组件的暴露机制方面是相关的。- 当
__isScriptSetup
标记为真(即组件是<script setup>
组件)时,__expose
的处理方式会受到影响。具体来说,<script setup>
组件默认情况下会自动暴露在<script setup>
内部定义的响应式数据和函数,而defineExpose
可以用于更精细地控制这种暴露。__isScriptSetup
标记作为一个前提条件,使得 Vue 能够正确地识别这种特殊的暴露机制,并根据__expose
(通过defineExpose
来控制)来确定最终暴露给外部的内容。
Vue3 相关源码位置:传送门。
解决问题
上面这段内容或许会相对复杂一些,因为它涉及到 Vue 的整体源码。然而,即便咱们不去深究源码部分,我们也能够较为直观地感受到两者之间的本质差异。相信呢,这下你对它们已经有了一个更为清晰的认识了。😀
然后,咱们再来说说如何解决 script setup
形式编写组件带来的问题。
上面小编提到可以通过 defineExpose 这个"宏"来解决,它允许开发者更精细地控制组件的API,只暴露需要让父组件访问的属性和方法,隐藏内部实现细节,从而增强了组件的封装性。
ScriptSetup.vue
文件:
<template>
<h1>{{ message }}</h1>
<h2>count:{{ count }}</h2>
<button @click="handleClick">点击</button>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const message = ref('我是script setup形式');
const count = ref(0);
function handleClick() {
count.value++;
}
// 主动暴露给外部调用
defineExpose({
message,
count,
handleClick
})
</script>
如此之后,咱们在 App.vue
中通过组件实例调用组件内部方法就不会报错了。
组件实例身上也能看到主动暴露出来的东西: