Vue defineModel 转 React:VuReact 怎么处理?

VuReact 是一个能将 Vue 3 代码编译为标准、可维护 React 代码的工具。今天就带大家直击核心:Vue 中常见的 defineModel 宏经过 VuReact 编译后会变成什么样的 React 代码?

前置约定

为避免示例代码冗余导致理解偏差,先明确几个小约定:

  1. 文中 Vue / React 代码均为核心逻辑简写,省略完整组件包裹、无关配置等内容;
  2. 默认读者已熟悉 Vue 3 中 defineModel 的 API 用法与核心行为;
  3. 仅支持 typedefaultrequired 选项及自定义 prop name;
  4. 不支持返回值使用数组解构方式。

编译对照

defineModel 基础用法

defineModel 是 Vue 3 <script setup> 中用于简化 v-model 双向绑定声明的宏。Vue 中 defineModel 会在编译时自动创建一个 ref 并生成对应的 modelValue prop 和 update:modelValue 事件。VuReact 会将它编译为 useVRef 将 prop 值转为响应式 ref,同时配合 useUpdated 实现值变化时自动触发 onUpdate:xxx 回调通知父组件。

  • Vue 代码:
ts 复制代码
<script setup lang="ts">
// 声明 "state" prop,由父组件通过 v-model:state 使用
const state = defineModel<string>('state');

// 声明带选项的 "modelValue" prop,由父组件通过 v-model 使用
const modelValue = defineModel({ default: 'xxx' });
</script>
  • VuReact 编译后 React 代码:
ts 复制代码
import { useVRef, useUpdated } from '@vureact/runtime-core';

type IChildProps = {
  state?: string;
  modelValue?: string;
} & {
  onUpdateState?: (arg: string) => void;
  onUpdateModelValue?: (arg: string) => void;
};

// 声明 "state" prop,由父组件通过 v-model:state 使用
const state = useVRef<string>(props.state);

// 声明带选项的 "modelValue" prop,由父组件通过 v-model 使用
const modelValue = useVRef<string>(props.modelValue ?? 'xxx');

// 值变化时自动通知父组件
useUpdated(() => {
  props.onUpdateState?.(state.value);
}, [state.value]);

useUpdated(() => {
  props.onUpdateModelValue?.(modelValue.value);
}, [modelValue.value]);

从示例可以看到:Vue 的 defineModel 被分解为三部分:

  1. prop 类型声明IChildProps 类型中的非事件字段(state?modelValue?
  2. 事件回调声明IChildProps 类型中的 onUpdateXxx 事件回调字段
  3. 运行时响应式useVRef 将 prop 初始值转为响应式 ref,useUpdated 监听值变化并自动调用父组件传入的回调

核心行为 :VuReact 保证组件内直接修改 defineModel ref 的 .value 就能触发父组件更新,与 Vue 开发体验完全一致。


defineModel 带选项

defineModel 支持传入 name 指定 prop 名称,以及 typedefaultrequired 选项。VuReact 会将这些选项转换为对应的 React 类型约束和默认值处理。

  • Vue 代码:
ts 复制代码
<script setup lang="ts">
// 声明带选项的 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel<number>('count', {
  type: Number,
  default: 0,
  required: true,
});
</script>
  • VuReact 编译后 React 代码:
ts 复制代码
type IChildProps = {
  count: number;  // required: true → 非可选类型
} & {
  onUpdateCount?: (arg: number) => void;
};

const count = useVRef<number>(props.count ?? 0); // default: 0 → 默认值回退

字段选项处理规则

  1. required: true :使 count 在类型定义中变为必填(count: number),而非可选(count?: number
  2. default: 0 :通过 ?? 空值合并运算符实现,在父组件未传递 prop 时使用默认值 0
  3. type: Number :影响类型定义中的泛型参数(<number>),VuReact 会利用此信息生成准确的 TypeScript 类型

modelValue 属性覆盖赋值

在 Vue 中,defineModel 返回的是一个 ref 对象,需要通过 .value 访问和修改状态;在 React 中,VuReact 编译后依然保留 .value 的访问方式。

  • Vue 代码:
ts 复制代码
<script setup lang="ts">
const state = defineModel<string>('state');

const update = () => {
  state.value = 'hello'; // 直接赋值修改
};
</script>
  • VuReact 编译后 React 代码:
ts 复制代码
const state = useVRef<string>(props.state);

const update = useCallback(() => {
  state.value = 'hello'; // 直接赋值修改,自动触发 props.onUpdateState 回调
}, [state.value]);

编译规则

  1. useCallback 包裹 :为涉及 state.value 的函数自动包裹 useCallback
  2. 正确依赖收集 :在依赖数组中正确添加 state.value
  3. 触发父组件更新state.value = 'hello' 可同时完成组件内状态更新和父组件的双向绑定同步

v-model 模板绑定

在 Vue 模板中,可以直接使用 v-model 绑定由 defineModel 声明的 ref;在 React 中,VuReact 会编译为受控组件的 value + onChange 模式。

  • Vue 代码:
html 复制代码
<template>
  <input v-model="modelValue" />
  <div>Parent bound v-model is: {{ count }}</div>
  <button @click="update">Increment</button>
</template>
  • VuReact 编译后 React 代码:
ts 复制代码
<input
  value={modelValue}
  onChange={(e) => {
    modelValue = e.target.value;
  }}
/>
<div>Parent bound v-model is:{count.value}</div>
<button onClick={update}>Increment</button>

v-model 转换规则

  1. value 绑定 :ref 的值绑定到 value 属性
  2. onChange 处理 :在 onChange 中直接修改 ref 值
  3. 自动同步 :值修改后触发 useUpdated 自动同步给父组件

不支持的 defineModel 用法

VuReact 明确不支持以下 defineModel 用法,编译时会跳过和报错。

1. 返回值数组解构

html 复制代码
<script setup lang="ts">
// 不支持的写法(Vue 3.4+ 实验特性)
const [arg1, arg2] = defineModel();
</script>

Vue 3.4+ 支持将 defineModel 返回值解构为 [model, modifiers] 元组,用于获取修饰符状态。VuReact 暂不支持此语法,建议使用标准写法:

html 复制代码
<script setup lang="ts">
// 支持的写法
const model = defineModel();
</script>

2. get / set / validator 选项

html 复制代码
<script setup lang="ts">
// 不支持的写法
const modelValue = defineModel({
  get() {},
  set() {},
  validator() {},
});
</script>

Vue 中 defineModel 支持 getset 自定义存取器和 validator 验证函数。VuReact 暂不支持这些选项,建议直接使用 useVRef 自行实现自定义逻辑。


编译策略总结

VuReact 的 defineModel 编译策略展示了完整的双向绑定转换能力

  1. Vue 宏分解 :将 defineModel 分解为类型声明、事件回调、运行时响应式三部分
  2. 选项支持 :支持 typedefaultrequired 等选项的转换
  3. 赋值习惯保留 :保持 .value 的访问和修改方式,与 Vue 开发体验一致
  4. 模板 v-model:编译为 React 标准的受控组件模式

核心功能

  1. 自动响应式 :通过 useVRef 将 prop 转为响应式 ref
  2. 自动通知 :通过 useUpdated 监听值变化并自动触发父组件回调
  3. 类型安全:根据选项自动推导 TypeScript 类型
  4. 函数缓存 :使用 useCallback 优化涉及 ref 操作的函数

注意事项

  1. 仅支持 prop namedefineModel('xxx') 形式传递 prop 名称
  2. 仅部分选项 :仅支持 typedefaultrequired 三个选项
  3. 不支持解构 :暂不支持 [model, modifiers] 数组解构语法
  4. 不支持存取器 :暂不支持 getsetvalidator 选项

VuReact 的编译策略确保了从 Vue 到 React 的平滑迁移,开发者无需手动实现双向绑定逻辑。编译后的代码既保持了 Vue 的 v-model 语义和 .value 赋值习惯,又符合 React 的受控组件设计模式,让迁移后的应用保持完整的双向绑定能力。

综合示例对照

完整的 defineModel 单文件组件编译前后对照,可参考以下代码:

  • Vue 代码(input.vue):
html 复制代码
<script setup lang="ts">
// @vr-name: Child

// 声明 "state" prop,由父组件通过 v-model:state 使用
const state = defineModel<string>('state');

// 声明带选项的 "modelValue" prop,由父组件通过 v-model 使用
const modelValue = defineModel({ default: 'xxx' });

// 声明带选项的 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel<number>('count', {
  type: Number,
  default: 0,
  required: true,
});

const update = () => {
  state.value = 'hello';
  count.value++;
};
</script>

<template>
  <input v-model="modelValue" />
  <div>Parent bound v-model is: {{ count }}</div>
  <button @click="update">Increment</button>
</template>
  • VuReact 编译后 React 代码(output.tsx):
ts 复制代码
import { useCallback, memo } from 'react';
import { useVRef, useUpdated } from '@vureact/runtime-core';

export type IChildProps = {
  state?: string;
  modelValue?: string;
  count: number;
} & {
  onUpdateState?: (arg: string) => void;
  onUpdateModelValue?: (arg: string) => void;
  onUpdateCount?: (arg: number) => void;
};

const Child = memo((props: IChildProps) => {
  const state = useVRef<string>(props.state);
  const modelValue = useVRef<string>(props.modelValue ?? 'xxx');
  const count = useVRef<number>(props.count ?? 0);

  const update = useCallback(() => {
    state.value = 'hello';
    count.value++;
  }, [state.value, count.value]);

  useUpdated(() => {
    props.onUpdateState?.(state.value);
  }, [state.value]);

  useUpdated(() => {
    props.onUpdateModelValue?.(modelValue.value);
  }, [modelValue.value]);

  useUpdated(() => {
    props.onUpdateCount?.(count.value);
  }, [count.value]);

  return (
    <>
      <input
        value={modelValue}
        onChange={(e) => {
          modelValue = e.target.value;
        }}
      />
      <div>Parent bound v-model is:{count.value}</div>
      <button onClick={update}>Increment</button>
    </>
  );
});

export default Child;

🔗 相关资源

📖 继续阅读


✨ 如果你觉得本文对你理解 VuReact 有帮助,欢迎点赞、收藏、关注!