各位观众,晚上好!今天咱们来聊聊 Vue 3 里那个神秘又重要的 setup
函数。这玩意儿啊,就像个魔法入口,组件里很多事情都得通过它来安排。别怕,咱们一步一步揭开它的面纱,保证你听完以后,也能玩转 setup
。
一、setup
函数的执行时机:早起的鸟儿有虫吃
setup
函数的执行时机非常关键,它在组件实例创建之前,并且只执行一次。你可以把它理解为组件的“初始化向导”,它会赶在组件的其他生命周期钩子之前,帮你把该准备的东西都准备好。
为了方便理解,咱们先来回顾一下 Vue 3 组件的生命周期(简化版):
生命周期钩子 | 执行时机 |
---|---|
beforeCreate |
组件实例初始化之后,props 解析/依赖注入之前。 |
setup |
在 beforeCreate 之后,created 之前。仅执行一次。 |
onBeforeMount |
组件挂载之前。 |
onMounted |
组件挂载之后。 |
onBeforeUpdate |
数据更新之前。 |
onUpdated |
数据更新之后。 |
onBeforeUnmount |
组件卸载之前。 |
onUnmounted |
组件卸载之后。 |
从表格里可以看到,setup
正好卡在 beforeCreate
和 created
之间。这意味着什么呢?
- 拿不到
this
: 因为组件实例还没创建好呢,this
当然是undefined
。想访问组件实例上的东西?抱歉,setup
里没门儿。 - 是时候准备数据了: 虽然拿不到
this
,但你可以用它来定义响应式数据、方法,这些都会在组件实例创建好之后,顺利地挂载到实例上。 - 依赖注入的好地方: 如果你用到了
provide/inject
,setup
里是接收注入数据的最佳时机。
二、setup
函数的参数:props
和 context
setup
函数接收两个参数:
-
props
: 一个对象,包含了父组件传递过来的所有props
。它是响应式的,这意味着如果父组件修改了props
,setup
函数里也能实时感知到。 -
context
: 一个对象,提供了一些有用的方法和属性。它不是响应式的。包含三个属性:attrs
:一个对象,包含了所有没有被props
声明的 attribute (比如class
和style
)。slots
:一个对象,包含了所有父组件传递过来的插槽。emit
:一个函数,用于触发自定义事件,让子组件可以和父组件通信。
咱们来看个例子:
// ParentComponent.vue
<template>
<ChildComponent :message="parentMessage" @custom-event="handleEvent" />
</template>
<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
setup() {
const parentMessage = ref('Hello from parent!');
const handleEvent = (data) => {
console.log('Event received:', data);
};
return {
parentMessage,
handleEvent
};
}
};
</script>
// ChildComponent.vue
<template>
<div>
<p>Message from parent: {{ message }}</p>
<p>Attribute: {{ attribute }}</p>
<button @click="handleClick">Click me</button>
<slot />
</div>
</template>
<script>
import { onMounted } from 'vue';
export default {
props: {
message: {
type: String,
required: true
}
},
setup(props, context) {
console.log('props:', props);
console.log('context:', context);
const attribute = context.attrs.customAttribute || 'Default Value'; //访问未声明attribute
const handleClick = () => {
context.emit('custom-event', 'Data from child');
};
onMounted(() => {
console.log('Child component mounted');
//你可以访问 props 的属性,比如 props.message
});
return {
handleClick,
attribute
};
}
};
</script>
在这个例子里:
ChildComponent
的setup
函数接收到了props
,可以访问message
属性。context.emit
用于触发custom-event
事件,并传递数据给父组件。context.attrs
可以访问父组件传递的没有在props声明的attribute,例如自定义的customAttribute
。
注意: 在 Vue 3.3 之后,props
解构赋值的默认行为发生了变化。简单来说,如果直接解构 props
,会导致解构出来的变量失去响应性。例如:
setup(props) {
const { message } = props; // 错误!message 不再是响应式的
// ...
}
正确的做法是使用 toRefs
或 toRef
来保持响应性:
import { toRefs, toRef } from 'vue';
setup(props) {
const { message } = toRefs(props); // 正确!message 是响应式的 ref 对象
// 或者
const messageRef = toRef(props, 'message'); // 正确!messageRef 是响应式的 ref 对象
// ...
}
toRefs
会将 props
对象的所有属性转换为 ref
对象,而 toRef
只转换指定的属性。选择哪个取决于你的需求。
三、setup
函数的返回值:通往模板的桥梁
setup
函数的返回值非常重要,它决定了哪些数据和方法可以被模板访问。返回值有三种情况:
-
返回一个对象: 这是最常见的用法。返回的对象里的属性和方法,都会被合并到组件实例上,可以在模板中直接使用。
setup() { const count = ref(0); const increment = () => { count.value++; }; return { count, increment }; }
在模板中:
<template> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </template>
-
返回一个函数: 这种用法比较少见,但也有它的用武之地。如果
setup
函数返回的是一个渲染函数,那么它会覆盖组件的模板。import { h } from 'vue'; setup() { return () => { return h('div', 'Hello from render function!'); }; }
这种方式可以完全控制组件的渲染过程,适合一些特殊的场景。
-
不返回任何东西: 这种情况下,
setup
函数不会向模板暴露任何数据或方法。但它仍然可以用来执行一些副作用操作,比如监听事件、发起网络请求等等。import { onMounted } from 'vue'; setup() { onMounted(() => { console.log('Component mounted!'); }); }
这种方式通常用于一些只需要在组件初始化时执行一次的操作。
四、关于响应式数据:ref
和 reactive
setup
函数里最常用的就是定义响应式数据了。Vue 3 提供了两种主要的 API:ref
和 reactive
。
-
ref
: 用于创建单个值的响应式引用。你可以把它理解为一个“盒子”,盒子里装着你想变成响应式的值。访问和修改ref
的值,需要通过.value
属性。import { ref } from 'vue'; setup() { const count = ref(0); const increment = () => { count.value++; }; return { count, increment }; }
-
reactive
: 用于创建对象的响应式代理。它会递归地将对象的所有属性都变成响应式的。访问和修改reactive
对象的属性,和普通对象一样。import { reactive } from 'vue'; setup() { const state = reactive({ name: 'Alice', age: 30 }); const updateName = (newName) => { state.name = newName; }; return { state, updateName }; }
选择 ref
还是 reactive
?
一般来说,如果你的数据是一个简单类型(比如数字、字符串、布尔值),或者只需要对单个值进行响应式处理,那么 ref
是个不错的选择。如果你的数据是一个复杂对象,并且需要对对象的多个属性进行响应式处理,那么 reactive
可能会更方便。
需要注意的是: reactive
只能用于对象类型,如果你尝试用它来包装一个简单类型的值,Vue 会发出警告。
五、setup
语法糖:<script setup>
Vue 3 还提供了一种更简洁的 setup
写法,叫做 <script setup>
。它是一种编译时的语法糖,可以让你更方便地使用 Composition API。
使用 <script setup>
之后,你可以直接在 <script>
标签里定义响应式数据、方法,而不需要显式地返回它们。编译器会自动帮你处理好一切。
<template>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
是不是感觉清爽多了?
<script setup>
的优点:
- 更简洁的代码: 不需要显式地返回数据和方法。
- 更好的性能: 编译器可以进行更多的优化。
- 更清晰的结构: 代码更易于阅读和维护。
需要注意的是: <script setup>
只能在单文件组件中使用。
六、最佳实践和常见问题
- 避免在
setup
里直接修改props
:props
是从父组件传递过来的,应该由父组件来控制。如果子组件需要修改props
的值,应该 emit 一个事件通知父组件,让父组件来修改。 - 谨慎使用
context.attrs
: 尽量使用props
来声明组件需要的属性。只有在确实无法预知属性名称的情况下,才考虑使用context.attrs
。 - 合理利用生命周期钩子:
setup
函数只执行一次,如果你需要在组件挂载、更新或卸载时执行一些操作,应该使用onMounted
、onBeforeUpdate
、onUnmounted
等生命周期钩子。 - 注意响应性丢失的问题: 特别是在解构
props
或reactive
对象时,要小心避免响应性丢失。
常见问题:
- 为什么我的数据没有更新? 可能是因为你没有正确地使用
ref
或reactive
,或者是因为你忘记了在模板中使用.value
访问ref
的值。 - 为什么我拿不到
this
? 因为setup
函数在组件实例创建之前执行,所以拿不到this
。 - 为什么我的
props
没有更新? 可能是因为父组件没有正确地传递props
,或者是因为你没有在setup
函数里正确地接收props
。 - 为什么我的事件没有触发? 可能是因为你没有正确地使用
context.emit
,或者是因为父组件没有监听对应的事件。
七、总结
setup
函数是 Vue 3 Composition API 的核心,掌握了它,你就掌握了组件开发的钥匙。记住它的执行时机、参数和返回值,合理利用 ref
和 reactive
,熟练使用 <script setup>
语法糖,你就能写出高效、可维护的 Vue 3 组件。
好啦,今天的讲座就到这里。希望大家都能成为 setup
大师!下次再见!