写在开头

作为一名 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 钩子中将它们打印了出来,经过小编的一阵翻找,确实有点差异存在, 如下:

Vue3,setup()函数与<script setup>到底有什么本质区别_API

Vue3,setup()函数与<script setup>到底有什么本质区别_App_02

可以看到通过 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 形式编写的组件却无法被调用,这个结局也是意料之中的事。

Vue3,setup()函数与<script setup>到底有什么本质区别_API_03

从源码角度看问题

至此,咱们得到了一个 "结论",只要记住这个结论就行了吧,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/#/ ,你会看到如下页面:

Vue3,setup()函数与<script setup>到底有什么本质区别_Vue_04

咱们可以点击想查看文件,它会给我们展示 Vue 文件编译后的情况。

如以 setup() 函数编写的组件情况:

Vue3,setup()函数与<script setup>到底有什么本质区别_API_05

能看到 template 模版直接被编译成一个 render 函数了,当然,这不是我们关注的重点,略过略过。来看 script 部分,大体还是与我们原来写的逻辑是一致的。

再来瞧瞧,以 script setup 形式编写的组件情况:

Vue3,setup()函数与<script setup>到底有什么本质区别_App_06

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 中通过组件实例调用组件内部方法就不会报错了。

组件实例身上也能看到主动暴露出来的东西:

Vue3,setup()函数与<script setup>到底有什么本质区别_API_07