在 Vue 3 中,v-model 是组件双向数据绑定的核心特性。随着 Vue 的版本演进,v-model 的使用方式也在不断优化。本文将基于您提供的代码示例,详细分析三种不同的 v-model 实现方式:基础用法、useVModel Hook(@vueuse/core组件库) 用法和 defineModel 宏用法。
基础用法是 Vue 3.4 版本之前的标准实现方式,需要手动处理 props 和 emits。
子组件实现 (myChild.vue):
点击数字增加
父组件使用:
3.4之前的v-model用法
{{ count }}---数值
特点:
需要显式定义 props 和 emits
通过 emit('update:xxx')
触发更新
兼容性好,适用于所有 Vue 3 版本
代码相对冗长
useVModel 是一种自定义 Hook(@vueuse/core组件库) 的封装方式,简化了双向绑定的实现。
useVModel源码 :
import type { Ref, UnwrapRef, WritableComputedRef } from 'vue'
import { computed, getCurrentInstance, nextTick, ref, watch } from 'vue'
import type { ToRefs } from 'vue';
export type CloneFn = (x: F) => T;
export function isDef(val?: T): val is T {
return typeof val !== 'undefined';
}
function cloneFnJSON(source: T): T {
return JSON.parse(JSON.stringify(source));
}
export interface UseVModelOptions {
/**
* When passive is set to `true`, it will use `watch` to sync with props and ref.
* Instead of relying on the `v-model` or `.sync` to work.
*
* @default false
*/
passive?: Passive;
/**
* When eventName is set, it's value will be used to overwrite the emit event name.
*
* @default undefined
*/
eventName?: string;
/**
* Attempting to check for changes of properties in a deeply nested object or array.
* Apply only when `passive` option is set to `true`
*
* @default false
*/
deep?: boolean;
/**
* Defining default value for return ref when no value is passed.
*
* @default undefined
*/
defaultValue?: T;
/**
* Clone the props.
* Accepts a custom clone function.
* When setting to `true`, it will use `JSON.parse(JSON.stringify(value))` to clone.
*
* @default false
*/
clone?: boolean | CloneFn;
/**
* The hook before triggering the emit event can be used for form validation.
* if false is returned, the emit event will not be triggered.
*
* @default undefined
*/
shouldEmit?: (v: T) => boolean;
}
export function useVModel<
P extends object,
K extends keyof P,
Name extends string
>(
props: P,
key?: K,
emit?: (name: Name, ...args: any[]) => void,
options?: UseVModelOptions
): WritableComputedRef
;
export function useVModel<
P extends object,
K extends keyof P,
Name extends string
>(
props: P,
key?: K,
emit?: (name: Name, ...args: any[]) => void,
options?: UseVModelOptions
): Ref>;
export function useVModel<
P extends object,
K extends keyof P,
Name extends string,
Passive extends boolean
>(
props: P,
key?: K,
emit?: (name: Name, ...args: any[]) => void,
options: UseVModelOptions = {}
) {
const {
clone = false,
passive = false,
eventName,
deep = false,
defaultValue,
shouldEmit,
} = options;
const vm = getCurrentInstance();
const _emit =
emit ||
vm?.emit ||
vm?.$emit?.bind(vm) ||
vm?.proxy?.$emit?.bind(vm?.proxy);
let event: string | undefined = eventName;
if (!key) {
key = 'modelValue' as K;
}
event = event || `update:${key!.toString()}`;
const cloneFn = (val: P[K]) =>
!clone ? val : typeof clone === 'function' ? clone(val) : cloneFnJSON(val);
const getValue = () =>
isDef(props[key!]) ? cloneFn(props[key!]) : defaultValue;
const triggerEmit = (value: P[K]) => {
if (shouldEmit) {
if (shouldEmit(value)) _emit(event, value);
} else {
_emit(event, value);
}
};
if (passive) {
const initialValue = getValue();
const proxy = ref
(initialValue!);
let isUpdating = false;
watch(
() => props[key!],
(v) => {
if (!isUpdating) {
isUpdating = true;
(proxy as any).value = cloneFn(v) as UnwrapRef
;
nextTick(() => (isUpdating = false));
}
}
);
watch(
proxy,
(v) => {
if (!isUpdating && (v !== props[key!] || deep)) triggerEmit(v as P[K]);
},
{ deep }
);
return proxy;
} else {
return computed
({
get() {
return getValue()!;
},
set(value) {
triggerEmit(value);
},
});
}
}
export function useVModels
(
props: P,
emit?: (name: Name, ...args: any[]) => void,
options?: UseVModelOptions,
): ToRefs
export function useVModels
(
props: P,
emit?: (name: Name, ...args: any[]) => void,
options?: UseVModelOptions,
): ToRefs
export function useVModels
(
props: P,
emit?: (name: Name, ...args: any[]) => void,
options: UseVModelOptions = {},
): ToRefs {
const ret: any = {};
for (const key in props) {
ret[key] = useVModel(
props,
key,
emit,
options as Parameters[3],
);
}
return ret;
}
子组件实现 (ProRadiuSelectSecond.vue):
{{ item.label }}
父组件使用:
useVmodelHooks使用
特点:
封装了 props 和 emits 的处理逻辑
提供更简洁的响应式变量访问
需要额外引入 useVModel 工具函数
适合在需要复用 v-model 逻辑的场景
defineModel 是 Vue 3.4 引入的新特性,极大简化了双向绑定的实现。
子组件实现 (ProRadiuSelect.vue):
{{ item.label }}
父组件使用:
v-model使用
特点:
代码最简洁,一行代码即可实现双向绑定
内置类型推断,开发体验好
需要 Vue 3.4 或更高版本
未来 Vue 官方推荐的方式
特性 | 基础用法 | useVModel Hook | defineModel 宏 |
---|---|---|---|
代码简洁度 | 低 | 中 | 高 |
Vue 版本要求 | 所有 | 所有 | 3.4+ |
类型支持 | 需要手动 | 需要手动 | 自动推断 |
额外依赖 | 无 | 需要 | 无 |
未来兼容性 | 高 | 中 | 高 |
Vue 3 中的 v-model 实现方式经历了从基础用法到 Hook 封装,再到现在的 defineModel 宏的演进过程。随着 Vue 的不断发展,双向绑定的实现变得越来越简洁和高效。开发者应根据项目实际情况选择合适的方式,在保证代码质量的同时提升开发效率。
新项目:如果使用 Vue 3.4+,优先选择 defineModel
宏,它提供了最简洁的语法和最佳的类型支持。
旧项目升级:
如果需要保持兼容性,可以使用 useVModel
Hook 作为过渡方案
逐步将基础用法迁移到 defineModel
使用uni或taro开发小程序时,不太推荐使用defineModel,兼容性较差
复杂场景:当需要处理复杂的双向绑定逻辑时,可以考虑使用 useVModel
进行自定义封装。
类型安全:无论使用哪种方式,都应该优先使用 TypeScript 以获得更好的类型检查和开发体验。