深入理解 Vue 3 的 “ 语法糖如何简化 Composition API 的使用,并讨论其编译时的转换过程。

大家好,我是老码,今天咱们来聊聊 Vue 3 里那个让人又爱又恨的 <script setup> 语法糖。 说它“爱”,是因为它真的能让你的 Vue 组件代码简洁到飞起;说它“恨”,是因为如果你不了解它背后的原理,很容易踩坑。

咱们今天的目标就是:彻底搞懂 <script setup>,让你用得顺心应手,再也不怕被它“糖”住了!

开胃小菜:<script setup> 是什么?

简单来说,<script setup> 是 Vue 3 提供的一个 语法糖,目的是让咱们用 Composition API 更加方便。 它是一个单文件组件(SFC)中 <script> 标签的一个属性。

如果没有 <script setup>,你要这样写:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

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

export default {
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment,
    };
  },
};
</script>

有了 <script setup>,代码直接变成这样:

<template>
  <div>
    {{ count }}
    <button @click="increment">Increment</button>
  </div>
</template>

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

const count = ref(0);

const increment = () => {
  count.value++;
};
</script>

看到没? setup 函数、return 语句,统统不见了! 代码瞬间清爽了很多。 这就是 <script setup> 的魅力所在。

正餐:<script setup> 的神奇之处

<script setup> 为什么能做到这么简洁? 这是因为它在编译时做了很多“魔法”。 咱们来逐一揭秘:

  1. 自动暴露:变量和函数自动暴露给模板

    <script setup> 里面声明的顶层变量和函数,都会被自动暴露给模板使用。 换句话说,你不需要再显式地 return 它们了。

    • 变量refreactivecomputed 等创建的响应式变量。
    • 函数: 普通的 JavaScript 函数。

    注意: 只有顶层声明的变量和函数才会被暴露。 如果你在一个函数内部声明变量,那它就只能在函数内部使用,不能在模板中使用。

    <template>
      <div>
        {{ message }}
        <button @click="handleClick">Click me</button>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    const message = ref('Hello, world!');
    
    const handleClick = () => {
      message.value = 'Button clicked!';
    };
    
    // 在函数内部声明的变量,无法在模板中使用
    const internalVariable = 'This is internal';
    </script>
  2. 自动注册组件

    如果你在 <script setup> 中导入了组件,那么这些组件会被自动注册,你可以在模板中直接使用它们,不需要再手动注册 components 选项。

    <template>
      <div>
        <MyComponent />
      </div>
    </template>
    
    <script setup>
    import MyComponent from './MyComponent.vue';
    </script>

    是不是很方便? 但是,要注意命名规范。 Vue 会根据组件的文件名来推断组件的名称。 建议使用 PascalCase 命名组件文件,比如 MyComponent.vue

  3. definePropsdefineEmits

    <script setup> 提供 definePropsdefineEmits 这两个编译器宏,用来声明组件的 props 和 emits。 它们不需要导入,可以直接使用。

    • defineProps: 声明 props。 它可以接收两种参数:

      • 对象形式: 类似于 Vue 2 的 props 选项,可以指定 props 的类型、是否必填、默认值等等。
      • 数组形式: 简单地声明 props 的名称,类型由 TypeScript 推断(如果使用了 TypeScript)。
      <script setup>
      // 对象形式
      const props = defineProps({
        message: {
          type: String,
          required: true,
        },
        count: {
          type: Number,
          default: 0,
        },
      });
      
      // 数组形式 (需要开启 TypeScript 支持)
      // const props = defineProps(['message', 'count']);
      </script>
    • defineEmits: 声明 emits。 它也接收两种参数:

      • 数组形式: 简单地声明 emits 的名称。
      • 对象形式 (推荐,需要 TypeScript 支持): 可以对 emit 的参数进行类型校验。
      <script setup>
      // 数组形式
      const emit = defineEmits(['update:modelValue', 'custom-event']);
      
      // 对象形式 (需要开启 TypeScript 支持)
      // const emit = defineEmits<{
      //   (e: 'update:modelValue', value: string): void
      //   (e: 'custom-event', id: number): void
      // }>()
      
      const handleClick = () => {
        emit('update:modelValue', 'new value');
        emit('custom-event', 123);
      };
      </script>
  4. defineExpose

    默认情况下,<script setup> 中的变量和函数都是私有的,不能在父组件中直接访问。 如果你想在父组件中访问子组件的某些变量或函数,可以使用 defineExpose 编译器宏。

    // 子组件
    <script setup>
    import { ref } from 'vue';
    
    const count = ref(0);
    
    const increment = () => {
      count.value++;
    };
    
    defineExpose({
      count,
      increment,
    });
    </script>
    
    // 父组件
    <template>
      <div>
        <ChildComponent ref="child" />
        <button @click="accessChild">Access Child</button>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    import ChildComponent from './ChildComponent.vue';
    
    const child = ref(null);
    
    const accessChild = () => {
      console.log(child.value.count);
      child.value.increment();
    };
    </script>
  5. useSlotsuseAttrs

    如果你的组件需要使用 slots 或 attrs,可以使用 useSlotsuseAttrs 这两个 API。 它们需要从 vue 中导入。

    <template>
      <div>
        <slot name="header"></slot>
        <p>Default content</p>
        <slot name="footer"></slot>
      </div>
    </template>
    
    <script setup>
    import { useSlots, useAttrs } from 'vue';
    
    const slots = useSlots();
    const attrs = useAttrs();
    
    console.log(slots);
    console.log(attrs);
    </script>

主菜:<script setup> 的编译过程

<script setup> 的简洁背后,是 Vue 编译器默默地为你做了很多事情。 咱们来看看它的编译过程:

  1. 解析 SFC: Vue 编译器首先会解析 SFC 文件,将 <template><script><style> 分别提取出来。

  2. 转换 <script setup>: 这是最关键的一步。 编译器会将 <script setup> 中的代码转换成标准的 JavaScript 代码。 主要包括以下几个步骤:

    • 包裹在 setup 函数中: 将 <script setup> 中的所有代码包裹在一个 setup 函数中。
    • 处理顶层声明
      • 将顶层变量和函数转换为 refreactive 变量(如果需要)。
      • 将这些变量和函数添加到 setup 函数的返回值中,以便暴露给模板。
    • 处理 definePropsdefineEmits: 将 definePropsdefineEmits 转换为 propsemits 选项。
    • 处理 defineExpose: 将 defineExpose 转换为 expose 选项。
    • 自动注册组件: 自动注册导入的组件。
  3. 生成渲染函数: 根据 <template> 生成渲染函数。

  4. 输出 JavaScript 代码: 将转换后的 JavaScript 代码输出。

为了更直观地理解这个过程,咱们来看一个例子:

<template>
  <div>
    {{ message }}
    <button @click="handleClick">Click me</button>
  </div>
</template>

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

const message = ref('Hello, world!');

const handleClick = () => {
  message.value = 'Button clicked!';
};
</script>

经过编译后,可能会变成类似这样的代码:

import { ref, defineComponent } from 'vue';

export default defineComponent({
  setup() {
    const message = ref('Hello, world!');

    const handleClick = () => {
      message.value = 'Button clicked!';
    };

    return {
      message,
      handleClick,
    };
  },
  render: // 省略渲染函数
});

可以看到,<script setup> 中的代码被包裹在 setup 函数中,messagehandleClick 被添加到 setup 函数的返回值中。

甜点:<script setup> 的注意事项

虽然 <script setup> 很好用,但是也有一些需要注意的地方:

  • 不能和 export default 并存<script setup> 已经隐式地导出了组件,所以不能再使用 export default。 如果你需要导出一些额外的东西,可以使用单独的 <script> 标签。

    <template>
      <div>
        {{ message }}
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    const message = ref('Hello, world!');
    </script>
    
    <script>
    // 可以导出一些额外的配置
    export default {
      name: 'MyComponent',
    };
    </script>
  • 顶层 await<script setup> 支持顶层 await,这意味着你可以在 <script setup> 中直接使用 await 关键字。 这在异步加载数据时非常方便。

    <template>
      <div>
        {{ data }}
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    const data = ref(null);
    
    const fetchData = async () => {
      const response = await fetch('/api/data');
      data.value = await response.json();
    };
    
    // 顶层 await
    await fetchData();
    </script>
  • TypeScript 支持<script setup> 和 TypeScript 是天生一对。 使用 TypeScript 可以获得更好的类型检查和代码提示。 强烈建议在 <script setup> 中使用 TypeScript。

    <template>
      <div>
        {{ message }}
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue';
    
    const message = ref<string>('Hello, world!');
    </script>
  • 命名冲突: 要避免在 <script setup> 中声明的变量和函数与模板中的变量和函数发生命名冲突。 如果发生冲突,编译器可能会报错。

  • 调试: 由于 <script setup> 是一个语法糖,所以在调试时可能会遇到一些困难。 建议使用 Vue Devtools 来调试 <script setup> 组件。

总结:<script setup>,让你的 Vue 代码更上一层楼

<script setup> 是 Vue 3 中一个非常强大的语法糖,它可以让你的 Composition API 代码更加简洁、易读。 但是,要真正掌握 <script setup>,你需要了解它背后的原理和注意事项。

希望今天的讲座能帮助你更好地理解 <script setup>,并在实际开发中灵活运用它,让你的 Vue 代码更上一层楼!

特性 描述
自动暴露 <script setup> 中声明的顶层变量和函数会自动暴露给模板使用,无需显式 return
自动注册组件 导入的组件会自动注册,无需手动在 components 选项中注册。
defineProps 用于声明组件的 props,支持对象形式和数组形式。
defineEmits 用于声明组件的 emits,支持数组形式和对象形式 (推荐,需要 TypeScript 支持)。
defineExpose 用于显式地暴露组件的变量和函数给父组件访问。
useSlots / useAttrs 用于在组件中使用 slots 和 attrs。
顶层 await 支持在 <script setup> 中直接使用 await 关键字。
TypeScript 支持 强烈建议在 <script setup> 中使用 TypeScript,以获得更好的类型检查和代码提示。
不能和 export default 并存 <script setup> 已经隐式地导出了组件,所以不能再使用 export default。 如果需要导出一些额外的配置,可以使用单独的 <script> 标签。
编译过程 编译器会将 <script setup> 中的代码转换成标准的 JavaScript 代码,包括包裹在 setup 函数中、处理顶层声明、处理 definePropsdefineEmits、处理 defineExpose、自动注册组件等等。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注