patch->processComponent(
1.setupComponent->setupComponent->setupStatefulComponent(
1.setup 2.handleSetupResult(
1. instance.setupState = proxyRefs(setupResult) (
shallowUnwrapHandlers->unref->Reflect.get
)
2. exposeSetupStateOnRenderContext(instance);(
toRaw(setupState)
Object.defineProperty(ctx, key,...
)
)
)->finishComponentSetup
2.setupRenderEffect->effect.run->componentUpdateFn->patch
)
工作机制(精简)
<script setup>
import { ref } from 'vue' const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
- 读取解包:模板编译后访问的是 _ctx.count。ctx.count 的 getter 返回 setupState.count;setupState 是 proxyRefs(...) 产生的代理,get trap 对取到的值执行 unref,因此直接得到 ref.value。
- 写入解包:如模板里的 count++ 或 count = 1,最终会对 setupState.count 触发 set trap;若旧值是 ref 且新值不是 ref,则自动执行 oldValue.value = newValue,从而对 ref.value 赋值。
这就是“模板中 ref 自动解包”的完整链路:setup 返回对象 → proxyRefs 包装 → 暴露到 ctx → 模板经由 ctx 访问,读取时 unref,写入时改 .value。
- 你的示例中,{{ count }} 展示时读取被 unref,而 @click="count++" 写入时走 set trap,从而修改 count.value。
- 小补充:expose() 暴露给外部时也会配合 proxyRefs 做相同的浅层解包(见 getComponentPublicInstance 路径)。
- 如果需要“深层”对象属性也自动解包,需要显式取 .value 或使用组合式 API 在合适处展开;运行时的 proxyRefs 仅做浅层处理。
- 以上行为仅作用在实例/模板上下文访问;在普通 JS 作用域直接用变量时,仍需手动 .value。
- 变更后的简要影响
- 模板中直接读写 ref:读等同 ref.value,写等同给 ref.value 赋值。
function unref(ref2) {
return isRef(ref2) ? ref2.value : ref2;
}
function toValue(source) {
return isFunction(source) ? source() : unref(source);
}
const shallowUnwrapHandlers = {
get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
} else {
return Reflect.set(target, key, value, receiver);
}
}
};
function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
function handleSetupResult(instance, setupResult, isSSR) {
if (isFunction(setupResult)) {
if (instance.type.__ssrInlineRender) {
instance.ssrRender = setupResult;
} else {
instance.render = setupResult;
}
} else if (isObject(setupResult)) {
if (isVNode(setupResult)) {
warn$1(
`setup() should not return VNodes directly - return a render function instead.`
);
}
{
instance.devtoolsRawSetupState = setupResult;
}
instance.setupState = proxyRefs(setupResult);
{
exposeSetupStateOnRenderContext(instance);
}
} else if (setupResult !== void 0) {
warn$1(
`setup() should return an object. Received: ${setupResult === null ? "null" : typeof setupResult}`
);
}
finishComponentSetup(instance, isSSR);
}
function exposeSetupStateOnRenderContext(instance) {
const { ctx, setupState } = instance;
Object.keys(toRaw(setupState)).forEach((key) => {
if (!setupState.__isScriptSetup) {
if (isReservedPrefix(key[0])) {
warn$1(
`setup() return property ${JSON.stringify(
key
)} should not start with "$" or "_" which are reserved prefixes for Vue internals.`
);
return;
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP
});
}
});
}