- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
导读
在 Vue 3 组件库 naive-ui 中,几乎每一个拥有值语义的组件(输入框、选择器、日期选择器、分页器等)都支持两种截然不同的工作方式:非受控模式(uncontrolled manner)与受控模式(controlled manner)。理解两者的区别、切换规则以及 naive-ui 特有的"以undefined判定非受控"约定,是正确使用表单类组件、避免"清空失效"等经典坑的关键。本文以官方文档《Controlled manner & uncontrolled manner》(英文版 / 中文版)为核心骨架,结合仓库源码深入剖析其底层实现,读完你将能:准确判断任意组件当前处于哪种模式、正确使用v-model与default-value、以及理解为什么undefined会切换模式而null才能清空值。
什么是受控模式与非受控模式
一个组件的行为可以分为两种模式:
- 非受控模式:你不设置组件的值属性,只监听它的变化。组件的值由组件自身维护,用户输入后值即时更新,你通过事件拿到最新值。
- 受控模式:你同时控制组件的值并监听它的变化。只有当你更新传入的
value时,组件的值才会改变;用户操作只会触发事件,最终展示的值完全由你的数据决定。
以 naive-ui 中最典型的<n-input />为例,两种模式的差异一目了然:
<!-- 非受控:值由组件自己管理,你只负责接收变化 --> <n-input @update:value="handleUpdateValue" /><!-- 受控:值由你的数据驱动,不更新 value 则组件值不变 --> <n-input :value="value" @update:value="handleUpdateValue" />非受控模式适合"只关心结果、不需要回写"的场景(如一次性表单提交前的即时收集),受控模式则适合需要联动校验、二次加工、跨组件同步的数据流场景。
v-model属于受控模式
Vue 3 中的v-model本质上是一个语法糖,它等价于同时绑定:model-value与@update:model-value:
<n-input v-model:value="value" /> <!-- 等价于 --> <n-input :value="value" @update:value="handleUpdateValue" />因此官方文档明确指出:使用v-model的组件一定工作在受控模式下,因为v-model就是:model-value与@update:model-value的组合。这一点对所有 naive-ui 组件通用。
naive-ui 如何区分两种模式:undefined即非受控
不同的组件库对"如何判定受控与否"有各自约定。naive-ui 的规则非常明确且统一:
只要组件的
value为undefined或未传入,该组件即处于非受控模式。
由此引出一个新手最容易踩的坑:
将一个受控组件的
value设为undefined,并不能清空它的值,只会把组件从受控模式切换成非受控模式。
因为在 naive-ui 看来,"值为undefined"与"根本没传值"是等价的——此时组件会退化为自管理状态,转而读取default-value(如果提供了的话),用户继续输入也不会受你的数据约束。若你想真正清空一个受控组件的值,绝大多数场景下应该使用null,因为null是一个"真实存在的值",组件会保持在受控模式并展示空内容。
不只是value
受控/非受控的机制并不局限于value一个属性。官方文档强调:任何形如xxx与@update:xxx的属性对,都可以同时以受控或非受控方式工作。
例如:
<!-- 非受控:组件的展开状态由自己维护 --> <n-collapse @update:expanded-names="handleUpdate" /> <!-- 受控:展开状态完全由你的数据驱动 --> <n-collapse :expanded-names="expandedNames" @update:expanded-names="handleUpdate" />类似的还有show/@update:show(弹层类组件)、checked/@update:checked(复选框)、page/@update:page(分页器)等。规则完全一致:传了值(非undefined)就是受控,不传或传undefined就是非受控。
源码级验证:受控模式的底层实现
naive-ui 的受控/非受控机制并非靠魔法,而是有一套统一的组合式函数(composable)支撑。以本文主角<n-input />为例,看 Input.tsx 中的核心实现:
// 内部自维护的值:以 defaultValue 初始化 const uncontrolledValueRef = ref(props.defaultValue) // 外部受控值:直接映射到 props.value const controlledValueRef = toRef(props, 'value') // 合并二者:useMergedState 负责判定最终以谁为准 const mergedValueRef = useMergedState( controlledValueRef, uncontrolledValueRef )这段代码来自 src/input/src/Input.tsx,其中的关键思路是:
uncontrolledValueRef以props.defaultValue初始化,对应文档中"非受控模式"的取值来源;controlledValueRef是对props.value的响应式引用,对应"受控模式"下由外部数据驱动的值;useMergedState(来自vooks工具库)将二者合并——当外部value为undefined时,它自动回退到内部uncontrolledValueRef;当外部传入了真实值(如null或具体字符串)时,则以外部值为准。
这正好从源码层面印证了文档的结论:"传undefined= 没传 = 非受控"是useMergedState合并逻辑的直接结果,而不是文档的"建议性约定"。
同样的模式在仓库中随处可见,例如 AutoComplete.tsx:
const uncontrolledValueRef = ref(props.defaultValue) const controlledValueRef = toRef(props, 'value') const mergedValueRef = useMergedState( controlledValueRef, uncontrolledValueRef )以及 BackTop.tsx 中通过useMergedState(controlledShowRef, uncontrolledShowRef)合并受控的show与内部滚动状态、Calendar.tsx 中以props.defaultValue || null初始化内部值——"受控值 + 内部默认值 + useMergedState 合并"是 naive-ui 几乎所有值型组件共用的标准范式。
组件自身如何"决定"回写
在非受控模式下,组件内部值变化后需要同步给自己。以 Input 为例,Input.tsx 中的处理逻辑为:当检测到当前处于非受控模式(外部未传入受控value)时,把新值写回uncontrolledValueRef,从而驱动 UI 更新;同时无论哪种模式都会通过@update:value把变化抛给外部监听者。这也解释了为什么非受控模式下"只监听变化"就能拿到实时值——组件在内部完成了"更新自己 + 通知外部"两步。
更复杂的场景:受控值与内部状态混合
有些组件的展示状态不完全等价于一个简单的value,naive-ui 会用"受控值 + 内部衍生状态"的组合。例如 MenuMask.tsx 内部维护uncontrolledShowRef,再通过合并逻辑决定最终show。这类内部工具组件同样遵循"非受控用内部 ref、受控用外部 props"的同一套心智模型,说明该约定是贯穿整个组件库的全局设计原则,而非某个组件的特例。
实战建议与常见陷阱
结合文档结论与源码行为,给出如下可直接落地的使用建议:
| 场景 | 推荐写法 | 模式 |
|---|---|---|
| 只收集用户输入,不回写 | <n-input @update:value="fn" /> | 非受控 |
| 数据由全局状态/父组件驱动 | <n-input :value="v" @update:value="fn" /> | 受控 |
| 双向绑定 | <n-input v-model:value="v" /> | 受控 |
| 提供初始值但不继续控制 | <n-input default-value="hi" @update:value="fn" /> | 非受控 |
| 清空受控组件的值 | v = null | 受控(保留模式) |
| 清空受控组件的值 | v = undefined⚠️ | 会切换为非受控,无法清空 |
需要特别留意的三点:
- 清空用
null,不要用undefined:这是 naive-ui 与部分组件库(它们以null判定非受控)的显著差异,迁移代码时最容易出错; default-value只在非受控模式下生效:一旦你传入受控value,default-value会被忽略(源码中uncontrolledValueRef只在初始时读取一次defaultValue);- 不要把受控值设为
undefined当作"重置"手段:若你确实需要临时脱离受控,请明确知道这等价于"切回非受控",组件的后续行为将不再受你的数据约束。
总结
naive-ui 的受控/非受控体系可以概括为一句话:"传值即受控,undefined即非受控"。它统一适用于所有xxx/@update:xxx属性对,底层由useMergedState(controlledRef, uncontrolledRef)这一标准范式实现(可参见 Input.tsx、AutoComplete.tsx 等实现)。掌握这套规则,你就能在表单、弹层、分页、折叠面板等所有场景中自由地在两种模式间切换,并避开"用undefined清空值"这一经典陷阱。更多细节可查阅官方文档的英文版与中文版,或直接阅读 src/input 等组件的源码深入学习。
- 前端
- UI组件
【免费下载链接】naive-ui
A Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.
相关推荐
naive-ui 受控模式与非受控模式完全指南:value、v-model 与 update 事件对的实战解析
naive ui 受控模式与非受控模式完全指南:value、v model 与 update 事件对的实战解析 本文以 naive ui 官方文档《受控模式与非
前端UI组件radix-vue 的 PopoverRoot 完全指南:受控/非受控状态、模态与非模态模式与源码原理
radix vue 的 PopoverRoot 完全指南:受控/非受控状态、模态与非模态模式与源码原理 导读 本文以 radix vue(即 Reka UI 前
前端UI组件设计系统ReactPy中的表单状态管理模式:受控与非受控组件
ReactPy中的表单状态管理模式:受控与非受控组件 在Web开发中,表单交互是用户体验的核心环节。作为Python开发者,你是否曾因表单状态同步问题而困扰?当
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考