vue3 响应式解包

33 阅读1分钟

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
      });
    }
  });
}