- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
本文以 naive-ui 官方文档《受控模式与非受控模式》为核心,结合
src/input等组件源码,系统讲解 Vue 3 组件中受控(controlled)与非受控(uncontrolled)两种模式的差异、naive-ui 特有的判定规则(undefined即非受控、清空用null)、v-model的本质,以及如何将xxx与@update:xxx属性对灵活运用于各类表单组件。读完本文,你将掌握受控模式的设计原理,能准确判断组件当前处于哪种模式,并在清空、回填、联动等真实场景中做出正确选择。
什么是受控模式与非受控模式
一个组件的行为可以分为受控模式和非受控模式两种:
- 非受控模式:只监听组件的变化,而不去控制组件的
value,组件值的变化由组件自身控制。即组件内部维护自己的状态,外部只负责“听”变化。 - 受控模式:既监听组件的变化,又控制组件的值。如果外部不更新
value,那么组件的值不会改变——组件值的变化由你(外部调用方)控制。
这一对概念在 React 生态中广为人知,naive-ui 作为 Vue 3 组件库同样完整实现了这一能力。理解两者的区别,是正确处理输入类组件(Input、Select、DatePicker 等)数据流的关键。
非受控模式:只监听,不控制
在非受控模式下,你不给<n-input />传value,只通过@update:value监听它的变化:
<n-input @update:value="handleUpdateValue" />此时组件的显示值由组件自身内部状态维护。用户每次输入,组件会更新内部值,同时触发update:value事件把最新值抛给外部。外部拿到新值后可以“用”它(比如存到变量、发请求),但不会把它回写进组件,因此组件的值始终跟随用户输入实时变化。
从 naive-ui 源码可以印证这一点:在 Input.tsx 中,组件内部维护了一个uncontrolledValueRef(初始化为props.defaultValue),并通过useMergedState将外部value与内部值合并:
// src/input/src/Input.tsx const uncontrolledValueRef = ref(props.defaultValue) const controlledValueRef = toRef(props, 'value') const mergedValueRef = useMergedState( controlledValueRef, uncontrolledValueRef )当没有传入value(即controlledValueRef为undefined)时,useMergedState会回退到uncontrolledValueRef,这正是“非受控 = 组件自己管值”的底层实现。
受控模式:既监听,又控制
在受控模式下,你同时做了两件事:监听组件的变化,并且把变化后的值回写给组件的value:
<n-input :value="value" @update:value="handleUpdateValue" />此时组件的值完全由你控制:
- 用户在输入框中敲字 → 组件触发
update:value事件; - 你在
handleUpdateValue中接收新值并更新value; - 组件通过
:value="value"拿到新值并重新渲染。
如果handleUpdateValue里不更新value,组件的值就不会改变——哪怕用户敲了字,输入框也会被“拉回”到旧的value上。这正是受控模式的核心特征:数据流的终点在外部。
受控模式的典型应用场景包括:输入值需要被其他逻辑校验、需要与其他组件联动、需要做格式化/过滤(如只允许数字、过滤敏感词)等。
v-model:受控模式的语法糖
v-model控制的组件同样工作在受控模式下,因为:
v-model等同于:model-value与@update:model-value的组合。
也就是说,下面两种写法在语义上完全等价:
<!-- 写法一:v-model --> <n-input v-model:value="value" /> <!-- 写法二:手动展开的受控模式 --> <n-input :value="value" @update:value="handleUpdateValue" />区别仅在于属性名:v-model默认绑定的是modelValue,而 naive-ui 的n-input等组件显式声明了value作为主值属性,因此需写作v-model:value(Vue 3 的v-model支持带参数的形式)。
对于以modelValue为默认 prop 的组件,直接写v-model即可。无论如何,只要通过v-model绑定,组件就处于受控模式——因为v-model既把值传进去(:model-value),又监听更新(@update:model-value),两者缺一不可。
naive-ui 中的受控模式判定规则
不同的组件库区分受控与非受控模式的方式是不同的。在 naive-ui 中,只要value是undefined或者根本没有传,那么组件的值就是非受控的。
这条规则非常关键,它带来一个容易踩坑的结论:
你将一个组件的值设为
undefined并不能清空它,只会把它的控制模式从受控切换为非受控。一般情况下,清空可以使用null。
原因在于:undefined在 naive-ui 中被视为“未传值”的哨兵值。当你写下:
<n-input :value="undefined" @update:value="handleUpdateValue" />组件解析 props 时拿到的value仍是undefined,与“根本没传value”在语义上无法区分,于是组件退回到内部uncontrolledValueRef维护的值——而这个内部值可能还保留着上一次的输入内容,因此输入框看起来“没被清空”,甚至用户还能继续正常输入(因为此时已经是非受控模式)。
而null是一个真实的值,传null意味着“我明确控制你的值为空”。此时:
- 组件仍处于受控模式(
value不是undefined); - 输入框显示为空;
- 用户输入后若不回写
value,输入框不会改变。
结合前面提到的源码:useMergedState只有在受控 ref 为undefined时才回退到内部值,而null是一个有效的受控值,因此null能正确表达“受控清空”的意图。
源码印证:受控值如何驱动视图
从 Input.tsx 的doUpdateValue可以看到事件触发后组件对值的处理:
function doUpdateValue(value, meta) { const { onUpdateValue, 'onUpdate:value': _onUpdateValue, onInput } = props if (onUpdateValue) call(onUpdateValue, value, meta) if (_onUpdateValue) call(_onUpdateValue, value, meta) if (onInput) call(onInput, value, meta) uncontrolledValueRef.value = value // 始终同步内部值 nTriggerFormInput() }注意最后一行:无论受控还是非受控,组件都会更新uncontrolledValueRef。这意味着:
- 在受控模式下,视图最终由外部
value决定(外部不更新,内部值更新了也没用); - 在非受控模式下,
uncontrolledValueRef的更新直接反映为视图变化。
这正是“受控与否取决于外部是否传value”这一规则的实现细节。组件本身并不区分两种模式,它只是忠实地同时维护内部状态、抛出事件,把选择权交给使用方。
不止value:任何xxx与@update:xxx属性对
受控/非受控模式并不局限于value这一个属性。任何xxx与@update:xxx的属性对都可以同时工作在受控或非受控模式下。
naive-ui 中大量组件都遵循这一约定,常见示例:
| 组件 | 属性对 | 说明 |
|---|---|---|
n-input | value/update:value | 输入值 |
n-select | value/update:value | 选中值 |
n-slider | value/update:value | 滑块当前值 |
n-tabs | value/update:value | 当前激活的 tab |
n-checkbox/n-radio | checked/update:checked | 勾选状态 |
n-switch | value/update:value | 开关状态 |
n-collapse | expandedNames/update:expanded-names | 展开的面板集合 |
n-menu | value(选中 key)/update:value | 菜单选中项 |
对于任意一个xxx属性对,规则与value完全一致:
- 不传
xxx(或传undefined):非受控,组件自己维护该状态,外部只监听update:xxx; - 传
xxx(且非undefined):受控,外部对该状态拥有最终决定权; - 需要主动清空受控状态:使用
null而不是undefined。
这种统一约定极大降低了学习成本:你只需要理解一套受控/非受控心智模型,就能套用到所有 naive-ui 组件的所有状态型属性上。例如n-collapse的expandedNames属性(在 Collapse.tsx 中同样通过类似机制实现):传expandedNames即为受控折叠面板,不传则由组件内部管理展开状态,同时通过update:expanded-names事件对外通知。
实战决策:如何选择受控与非受控
优先使用非受控(简单场景)
当组件值只是“临时收集”,不需要外部介入修改时,优先使用非受控模式:
<!-- 非受控:只收集输入 --> <n-input placeholder="请输入用户名" @update:value="username = $event" /> <!-- 使用 defaultValue 设置初始值,之后交给组件自己管 --> <n-input :default-value="'初始内容'" @update:value="handleChange" />注意defaultValue(见 Input.tsx,类型为null | string | [string, string],默认null)是非受控模式下设置初始值的途径——它只在组件首次创建时生效,后续外部更新defaultValue不会改变组件的显示值。
需要受控(联动、校验、格式化场景)
当值需要被外部逻辑“钳制”时使用受控模式:
<script setup> import { ref } from 'vue' const value = ref('') // 只允许输入数字 function handleUpdateValue(v) { value.value = v.replace(/\D/g, '') } </script> <template> <!-- 受控:用户输入非数字会被立即过滤掉 --> <n-input :value="value" @update:value="handleUpdateValue" /> </template>由于handleUpdateValue对值做了过滤后再写回value,任何非法字符都无法在输入框中停留——这是非受控模式做不到的。
清空受控值的正确姿势
受控模式下清空,请传null:
<script setup> import { ref } from 'vue' import { NButton, NInput } from 'naive-ui' const value = ref('hello') function clear() { value.value = null // 正确:受控模式下清空 // value.value = undefined // 错误:会让组件退化为非受控,界面不会被清空 } </script> <template> <n-input :value="value" @update:value="value = $event" /> <n-button @click="clear">清空</n-button> </template>如果把value.value设为undefined,组件会从受控切换为非受控,内部残留的旧值会让输入框“看起来没被清空”,这与大多数人的直觉相悖,是 naive-ui 使用中最高频的误区之一。
小结
- 非受控模式:不传
value(或传undefined),只监听@update:value,组件自己维护值; - 受控模式:传
value并监听@update:value,值的最终决定权在外部,不更新value则界面不变; v-model是受控模式的语法糖,等价于:model-value+@update:model-value;- naive-ui 判定规则:
value === undefined视为非受控;清空受控值请用null; - 通用性:任何
xxx/@update:xxx属性对都遵循同一套规则,覆盖 select、tabs、switch、collapse、menu 等几乎所有状态型组件。
掌握了这套规则,你在 naive-ui 中处理任何组件状态时都能明确判断当前处于哪种模式,并写出行为可预期、数据流清晰的代码。相关文档原文见 demo/pages/docs/controlled-uncontrolled/zhCN/index.md(英文版见 demo/pages/docs/controlled-uncontrolled/enUS/index.md),核心实现可进一步阅读 Input.tsx 中useMergedState与doUpdateValue相关代码。
- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
相关推荐
ant-design ColorPicker 受控模式完全指南:value 与 onChange 的实战与源码解析
ant design ColorPicker 受控模式完全指南:value 与 onChange 的实战与源码解析 导读 在 ant design 的 Colo
前端UI组件设计系统radix-vue 的 PopoverRoot 完全指南:受控/非受控状态、模态与非模态模式与源码原理
radix vue 的 PopoverRoot 完全指南:受控/非受控状态、模态与非模态模式与源码原理 导读 本文以 radix vue(即 Reka UI 前
前端UI组件设计系统攻克React组件状态难题:Reactstrap受控与非受控模式全解析
攻克React组件状态难题:Reactstrap受控与非受控模式全解析 你是否还在为React组件的状态管理头疼?表单提交时数据不同步?模态框突然无法关闭?本文
UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考