Skip to content

setup 函数

1.1 与 Vue 2 的选项式 API 相比有什么优势?

js
// 🤔 问题:setup 函数与 Vue 2 的选项式 API 相比有什么优势?
// ✅ 答案:
// 1. 更好的代码组织:相关逻辑可以放在一起,而非分散在不同选项中
// 2. 更好的逻辑复用:通过组合式函数复用逻辑,替代 mixins
// 3. 更好的类型推导:对 TypeScript 支持更友好
// 4. 更小的生产包体积:代码更容易被压缩和优化

export default {
  setup() {
    // 在这里编写组合式 API 代码
    return {
      // 返回的内容可在模板中使用
    };
  },
};

1.2 核心特点

面试题:setup 函数的核心特点是什么?在使用时需要注意哪些问题?

js
// 🤔 问题:setup 函数的执行时机和访问限制是什么?
// ✅ 答案:
// 1. 执行时机:在组件实例创建之前执行
// 2. 没有 this:因为在实例创建前执行,所以无法访问组件实例
// 3. 返回类型:可以返回对象或渲染函数
// 4. 参数:接收 props 和 context 两个参数

export default {
  setup(props, context) {
    // props 是响应式的
    console.log(props.title);

    // context 包含三个属性
    const { attrs, slots, emit } = context;

    return {
      // 返回的属性可在模板中访问
    };
  },
};

2. setup 函数的两种使用方式

2.1 作为组件选项

面试题:setup 函数的两种使用方式各有什么特点和应用场景?

js
// 🤔 问题:setup 选项的写法有什么优缺点?
// ✅ 答案:
// 优点:
// 1. 可以与选项式 API 共存,便于渐进式升级
// 2. 可以访问 props 和 emit 事件
// 缺点:
// 1. 需要手动返回暴露给模板的变量和方法
// 2. 在复杂组件中代码可能较长

export default {
  props: {
    title: String,
  },
  setup(props, { emit }) {
    // 1. 导入响应式 API
    const { ref, reactive, computed, watch } = Vue;

    // 2. 定义响应式状态
    const count = ref(0);
    const user = reactive({
      name: "Zhang San",
      age: 30,
    });

    // 3. 定义计算属性
    const doubleCount = computed(() => count.value * 2);

    // 4. 定义方法
    function increment() {
      count.value++;
      emit("increment", count.value);
    }

    // 5. 监听变化
    watch(count, (newValue, oldValue) => {
      console.log(`Count changed from ${oldValue} to ${newValue}`);
    });

    // 6. 生命周期钩子
    onMounted(() => {
      console.log("Component mounted");
    });

    // 7. 返回暴露给模板的内容
    return {
      count,
      user,
      doubleCount,
      increment,
    };
  },
};

2.2 <script setup> 语法糖

ts
// 🤔 问题:`<script setup>` 与普通 setup 函数有什么区别?为什么它是推荐的写法?
// ✅ 答案:
// 1. 更简洁:不需要手动 return,所有顶层变量和导入都会自动暴露给模板
// 2. 更高效:编译时优化,减少运行时开销
// 3. 更好的类型推导:特别是对 defineProps 和 defineEmits
// 4. 更好的 IDE 支持:更准确的自动补全

// <script setup> 基本使用示例
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// Props 定义 - 会自动暴露给模板
const props = defineProps({
  title: String,
  items: Array
})

// Emits 定义
const emit = defineEmits(['update', 'delete'])

// 响应式状态
const count = ref(0)
const user = reactive({
  name: 'Li Si',
  age: 25
})

// 计算属性
const doubleCount = computed(() => count.value * 2)

// 方法
function increment() {
  count.value++
  emit('update', count.value)
}

// 监听器
watch(count, (newVal) => {
  console.log(`Count is now: ${newVal}`)
})

// 生命周期钩子
onMounted(() => {
  console.log('Component is mounted')
})

// 所有顶层变量和函数都会自动暴露给模板使用
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <p>Double Count: {{ doubleCount }}</p>
    <button @click="increment">Increment</button>
    <ChildComponent :user="user" />
  </div>
</template>

3. setup 函数中的响应式系统

3.1 ref 与 reactive

面试题:Vue 3 的 ref 和 reactive 有什么区别?什么场景下应该使用哪一种?

js
// 🤔 问题:ref 和 reactive 的区别是什么?使用时有什么注意事项?
// ✅ 答案:
// ref:
// - 可以包装任何类型的值,包括基本类型
// - 需要通过 .value 访问和修改值
// - 在模板中会自动解包,不需要 .value
// reactive:
// - 只能用于对象类型(对象、数组、集合类型)
// - 直接使用属性访问和修改
// - 有解构丢失响应性的问题

// ref 示例
const count = ref(0);
count.value++; // 修改需要 .value

// reactive 示例
const state = reactive({
  count: 0,
  user: { name: "Wang Wu" },
});
state.count++; // 直接修改属性

// 解构问题与解决方案
const { count } = reactive({ count: 0 }); // ❌ 解构后丢失响应性
const count = toRef(state, "count"); // ✅ 保持响应性的解构
const { count } = toRefs(state); // ✅ 批量保持响应性的解构

3.2 计算属性与监听器

js
// 🤔 问题:在 setup 中如何实现计算属性和侦听器?它们与选项式 API 有什么不同?
// ✅ 答案:
// computed:
// - 可以创建基于其他响应式状态的派生状态
// - 返回一个只读的响应式引用
// - 可以通过 getter 和 setter 创建可写的计算属性
// watch/watchEffect:
// - watch 侦听一个或多个响应式数据源,并在数据源变化时触发回调
// - watchEffect 自动追踪依赖并在依赖变化时执行回调

// 计算属性
const firstName = ref("John");
const lastName = ref("Doe");

// 只读计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

// 可写计算属性
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(" ");
  },
});

// 监听单个数据源
watch(firstName, (newValue, oldValue) => {
  console.log(`firstName changed from ${oldValue} to ${newValue}`);
});

// 监听多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log("Name changed");
});

// watchEffect 会自动追踪依赖
watchEffect(() => {
  console.log(`Current name: ${firstName.value} ${lastName.value}`);
});

4. 组合式函数 (Composables)

面试题:Vue 3 中的组合式函数是什么?它如何解决代码复用问题?

js
// 🤔 问题:什么是组合式函数?它与 Vue 2 的 mixins 相比有什么优势?
// ✅ 答案:
// 组合式函数特点:
// 1. 是一个利用 Vue 组合式 API 的可复用函数
// 2. 命名约定为 useXxx
// 3. 返回一个包含状态和方法的对象
// 4. 可以互相组合,形成强大的逻辑复用机制
// 优势(相比 mixins):
// 1. 来源清晰:可以明确知道某个属性来自哪个组合式函数
// 2. 命名冲突:通过解构重命名避免冲突
// 3. 更好的类型推导:对 TypeScript 更友好

// 创建一个组合式函数
function useCounter(initialValue = 0) {
  const count = ref(initialValue);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return {
    count,
    increment,
    decrement,
  };
}

// 使用组合式函数
import { useCounter } from "./composables/counter";

export default {
  setup() {
    // 在组件中使用组合式函数
    const { count, increment } = useCounter(10);

    // 可以组合多个组合式函数
    const { user, loading } = useUser();

    // 通过解构重命名避免命名冲突
    const { count: messageCount } = useMessages();

    return {
      count,
      messageCount,
      increment,
      user,
      loading,
    };
  },
};

5. 最佳实践与常见问题

5.1 避免的反模式

js
// 🤔 问题:在使用 setup 函数时有哪些常见的反模式和陷阱?
// ✅ 答案:

// ❌ 错误:在 reactive 对象上解构属性
const state = reactive({ count: 0 });
const { count } = state; // 解构后丢失响应性
count++; // 不会更新视图

// ✅ 正确:使用 toRefs 保持响应性
const state = reactive({ count: 0 });
const { count } = toRefs(state);
count.value++; // 正确更新

// ❌ 错误:直接修改 ref 数组的元素
const list = ref(["A", "B"]);
list.value[0] = "X"; // 视图不会更新

// ✅ 正确:使用数组方法
list.value.splice(0, 1, "X");
// 或者
list.value = ["X", "B"];

5.2 性能优化建议

js
// 🤔 问题:使用 setup 时如何优化组件性能?
// ✅ 答案:

// 1. 使用 shallowRef 和 shallowReactive 处理大型对象
const bigObject = shallowRef({
  // 大量的嵌套数据
})

// 2. 使用 v-once 处理一次性渲染的内容
<template>
  <div v-once>{{ staticContent }}</div>
</template>

// 3. 使用计算属性缓存复杂计算
const filteredItems = computed(() => {
  return items.value.filter(item => item.isActive)
})

// 4. 使用 v-memo 减少不必要的模板更新
<div v-memo="[item.id]">
  {{ item.name }}
</div>

5.3 与 TypeScript 集成

ts
// 🤔 问题:如何在 setup 中更好地使用 TypeScript?
// ✅ 答案:

// 为 props 定义类型
const props = defineProps<{
  title: string;
  count?: number;
}>();

// 为 emits 定义类型
const emit = defineEmits<{
  (e: "update", value: number): void;
  (e: "delete", id: string): void;
}>();

// 为 ref 定义类型
const name = ref<string>("");

// 为 reactive 定义类型
interface User {
  id: number;
  name: string;
  email: string;
}

const user = reactive<User>({
  id: 1,
  name: "Zhang San",
  email: "zhangsan@example.com",
});

// 为函数返回值定义类型
function fetchUser(): Promise<User> {
  return axios.get("/api/user");
}

基于 MIT 许可发布