在之前的组件渲染文章中,我们简单介绍了 setup 函数的基本使用。今天,我们将深入探讨 setup 函数的执行上下文——它是什么时候被调用的?它接收什么参数?返回值如何处理?以及它在 Vue3 内部是如何实现的。理解这些,将帮助我们更好地掌握组合式 API 的精髓。
前言:setup 的设计初衷
在 Vue2 中,我们使用选项式 API 组织代码:
export default {
data() { return { count: 0 } },
computed: { double() { return this.count * 2 } },
methods: { increment() { this.count++ } },
mounted() { console.log('mounted') }
}
这种方式的痛点在于:相关逻辑被强制分散在不同的选项中。比如一个计数器的逻辑可能分散在 data、computed、methods、mounted 中。
Vue3 的组合式 API 通过 setup 函数解决了这个问题:
export default {
setup() {
// 计数器逻辑集中在一起
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
return { count, double, increment };
}
}
setup 函数是组合式 API 的入口,它在组件创建之前执行,并返回要暴露给模板的内容。
setup 的调用时机
在组件生命周期中的位置
setup 函数在组件实例创建之前执行,具体来说是在 beforeCreate 钩子之前:
export default {
beforeCreate() {
console.log('beforeCreate');
},
setup() {
console.log('setup');
const count = ref(0);
return { count };
},
created() {
console.log('created');
}
}
上述代码的打印结果是:
setup
beforeCreate
created
为什么要在beforeCreate之前执行?
setup 为什么要在 beforeCreate 之前执行,这背后有几个重要原因:
- 为了能够在
setup中使用响应式 API,这些 API 需要在组件实例上注册 - 为了能够访问 props 参数(参数可以被访问,但此时还没有被初始化)
- 为了能够提前注册生命周期钩子,如 onMounted 等
setup 参数解析
setup 函数可以接收两个参数:props 和 context。
props 参数
setup 可以接收的第一个参数,是响应式的 props 对象:
export default {
props: {
title: String,
count: Number
},
setup(props) {
// ✅ 可以访问props
console.log(props.title);
// ❌ 不能修改props
props.title = 'new title'; // 会触发警告
// ✅ 通过toRefs转换为响应式引用
const { title, count } = toRefs(props);
console.log(title.value); // 需要.value访问
// 或者单独转换某个属性
const titleRef = toRef(props, 'title');
return { title, count };
}
}
props的特殊性
- props是响应式的,但解构会失去响应性,可以使用toRefs保持响应性:
const { title, count } = toRefs(props); props被处理为shallowReactive
context 参数
setup 可以接收的第二个参数是上下文对象 context,包含四个属性:
- attrs: 非props的属性
- slots: 插槽
- emit: 触发事件
- expose: 暴露公共方法
attrs 和 slots 的特殊性
attrs 和 slots 是有状态的对象,它们会随着组件更新而更新:
setup(props, { attrs, slots }) {
// ❌ 解构会失去响应性
const { class } = attrs; // 不会响应更新
// ✅ 直接使用attrs
console.log(attrs.class); // 总是最新的
// ✅ 使用计算属性包装
const className = computed(() => attrs.class);
// slots同理
useEffect(() => {
console.log('slots变化了', slots);
});
}
// attrs的内部实现
function setupContext(instance) {
return {
// attrs是响应式的
get attrs() {
return instance.attrs;
},
// slots也是响应式的
get slots() {
return instance.slots;
},
emit: instance.emit,
expose: (exposed) => {
instance.exposed = exposed;
}
};
}
expose 的使用场景
expose 用于控制组件暴露的公共方法:
// 子组件
export default {
setup(props, { expose }) {
const count = ref(0);
const increment = () => count.value++;
const reset = () => count.value = 0;
const getCount = () => count.value;
// 只暴露reset方法给父组件
expose({
reset,
getCount
// increment没有被暴露
});
return { count, increment }; // 这些只用于模板
}
}
// 父组件
<template>
<Child ref="childRef" />
</template>
<script setup>
const childRef = ref();
// 只能访问到暴露的方法
childRef.value?.reset(); // ✅ 可以
childRef.value?.increment(); // ❌ undefined
</script>
setup 返回值的处理
setup 函数可以返回两种类型的值:对象或函数。
返回对象
setup 返回值最常见的情况就是返回一个对象,对象的属性会被暴露给模板:
<script>
export default {
setup() {
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
// 返回对象,模板中可以访问这些属性
return {
count,
double,
increment
};
}
}
</script>
<template>
<!-- 模板中可以访问返回的属性 -->
<div>
<p>count: {{ count }}</p>
<p>double: {{ double }}</p>
<button @click="increment">+1</button>
</div>
</template>
返回对象的处理过程
function handleSetupResult(instance, setupResult) {
if (typeof setupResult === 'object' && setupResult !== null) {
// 1. 标记为refs(用于模板解包)
instance.setupState = proxyRefs(setupResult);
// 2. 将属性合并到实例代理上
// 这样在模板中可以直接使用count而不是setupState.count
}
}
// proxyRefs的实现
function proxyRefs(target) {
return new Proxy(target, {
get(target, key) {
const value = Reflect.get(target, key);
// 自动解包ref
return isRef(value) ? value.value : value;
},
set(target, key, value) {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
// 如果是ref,设置其value属性
oldValue.value = value;
return true;
} else {
return Reflect.set(target, key, value);
}
}
});
}
返回函数
如果 setup 返回一个函数,这个函数会被作为渲染函数使用:
export default {
props: ['title'],
setup(props) {
const count = ref(0);
// 返回渲染函数
return () => {
return h('div', [
h('h1', props.title),
h('p', `count: ${count.value}`),
h('button', {
onClick: () => count.value++
}, '+1')
]);
};
}
}
注:不能在
setup中同时使用template和返回渲染函数。
返回函数的场景:
- 需要完全控制渲染逻辑
- 渲染逻辑依赖响应式数据
- 在渲染函数中使用JSX
// JSX示例 setup() { const items = ref([]); return () => ( <div> {items.value.map(item => ( <div key={item.id}>{item.text}</div> ))} </div> ); }
返回值的合并优先级
setup 返回值、data、methods 等的合并优先级:
export default {
data() {
return {
message: 'from data',
count: 0
};
},
setup() {
const count = ref(100); // 同名属性
const message = 'from setup';
return {
count,
message
// 这里的message会覆盖data中的message
};
},
computed: {
double() {
return this.count * 2; // 这里的count来自setup
}
}
}
最终实例上的属性合并顺序:
- props (最高优先级,不能被覆盖)
- setup返回的对象
- data
- computed
- methods
setup 中的 this
为什么 setup 中没有 this?
在 Vue2 中,我们习惯使用 this 访问组件实例:
export default {
data() { return { count: 0 } },
methods: {
increment() {
this.count++; // 使用this
}
}
}
但在setup中,是没有 this 的:
export default {
setup() {
console.log(this); // undefined
const count = ref(0);
function increment() {
// 不能使用this.count
count.value++; // 直接使用变量
}
return { count, increment };
}
}
为什么呢?其原因有以下几点:
- 执行时机:
setup执行在所有钩子函数之前,组件实例尚未完全创建 - 类型推导:避免
this带来的类型推导困难 - 解耦:让逻辑更独立,不依赖组件实例
如何获取组件实例?
虽然 setup 中没有 this,但可以通过 getCurrentInstance 获取当前组件实例:
import { getCurrentInstance } from 'vue';
export default {
setup() {
// 获取当前组件实例
const instance = getCurrentInstance();
console.log(instance); // 组件实例对象
console.log(instance.proxy); // 代理对象(模板中使用的this)
// 访问实例上的属性
console.log(instance.props);
console.log(instance.attrs);
// 注意:这个函数只能在setup或生命周期钩子中调用
onMounted(() => {
const instance = getCurrentInstance();
console.log('在钩子中也可以获取', instance);
});
return {};
}
}
注:getCurrentInstance 只能在同步代码中使用,不能在异步代码中使用:
setTimeout(() => { const instance = getCurrentInstance(); }, 100);此时instance结果为null
什么时候使用 getCurrentInstance
在 Vue 官方文档中,并不推荐频繁使用 getCurrentInstance,但在某些场景下我们确实需要使用这个方法,比如:
- 开发工具/调试
- 访问全局属性
- 插件开发
源码对标:setupComponent 函数
Vue3 源码中的 setupComponent
让我们深入 Vue3 源码,看看 setup 是如何被调用的,源码位置:packages/runtime-core/src/component.ts:
export function setupComponent(instance) {
// 1. 初始化props和slots
initProps(instance, instance.vnode.props);
initSlots(instance, instance.vnode.children);
// 2. 设置有状态组件(执行setup)
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
if (setup) {
// 创建setup上下文
const setupContext = createSetupContext(instance);
// 设置当前实例(用于getCurrentInstance)
setCurrentInstance(instance);
// 执行setup
const setupResult = setup(
instance.props, // 第一个参数:props
setupContext // 第二个参数:context
);
// 清理当前实例
setCurrentInstance(null);
// 处理返回值
handleSetupResult(instance, setupResult);
} else {
// 没有setup,直接完成组件初始化
finishComponentSetup(instance);
}
}
function createSetupContext(instance) {
return {
// 使用getter保持响应性
get attrs() {
return instance.attrs;
},
get slots() {
return instance.slots;
},
emit: instance.emit,
expose: (exposed) => {
instance.exposed = exposed;
}
};
}
function handleSetupResult(instance, setupResult) {
if (typeof setupResult === 'function') {
// 返回函数:作为渲染函数
instance.render = setupResult;
} else if (typeof setupResult === 'object' && setupResult !== null) {
// 返回对象:作为模板上下文
instance.setupState = proxyRefs(setupResult);
}
// 完成组件初始化
finishComponentSetup(instance);
}
function finishComponentSetup(instance) {
const Component = instance.type;
// 如果还没有render函数,尝试从模板编译
if (!instance.render) {
if (Component.template) {
instance.render = compile(Component.template);
}
}
}
完整的执行流程
常见陷阱与最佳实践
props解构陷阱
// ❌ 错误:解构后失去响应性
export default {
props: ['user'],
setup({ user }) {
// user不是响应式的
watch(user, () => {}); // 不会触发
return { user };
}
}
// ✅ 正确:使用toRefs
import { toRefs } from 'vue';
export default {
props: ['user'],
setup(props) {
const { user } = toRefs(props);
watch(user, () => {
console.log('user变化了', user.value);
});
return { user };
}
}
// ✅ 或者直接使用props
setup(props) {
watch(() => props.user, () => {
console.log('user变化了');
});
}
异步 setup 的处理
// ❌ 错误:setup不能是async
export default {
async setup() {
const data = await fetchData(); // 这会导致问题
return { data };
}
}
// ✅ 正确:在<script setup>中使用await
<script setup>
const data = await fetchData(); // 自动支持async
</script>
// ✅ 或者使用组合式函数
function useData() {
const data = ref(null);
onMounted(async () => {
data.value = await fetchData();
});
return data;
}
export default {
setup() {
const data = useData();
return { data };
}
}
生命周期钩子的注册
export default {
setup() {
// ✅ 直接在setup中注册
onMounted(() => {});
onUpdated(() => {});
onUnmounted(() => {});
// ❌ 不要在条件语句中注册
if (someCondition) {
onMounted(() => {}); // 顺序可能混乱
}
// ❌ 不要在异步中注册
setTimeout(() => {
onMounted(() => {}); // 不会生效
}, 100);
}
}
结语
理解 setup 的执行上下文,是掌握 Vue3 组合式 API 的关键。它不仅帮助我们写出更清晰的代码,也为深入理解 Vue3 的响应式系统打下基础。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!