管什么:
input、textarea、search、password、input-number、checkbox、radio、switch、segmented、slider、rate、color-picker这一族「自己持有一个有类型的值、不持有选项数据管道」的控件怎么选、它们的坑分别记在哪一篇的哪一个编号。不管什么:带选项归一与下拉浮层的一族(C05 类,入口 C05-00)、表单容器与校验引擎(C06-01)、受控标准件本体(I13)、原生输入基座(I04)、样式引擎与 Token 分层(A03 与 A04)、浮层挂载与层级编排(C12-06 与 I10)。
本类的十二份实现文件(9 个公开控件目录,加 vc-input、vc-checkbox、vc-slider 三个底座)解的是同一个抽象问题:把一次用户手势写进一个有类型的值,并回答「那份可写状态的第一个持有者究竟是谁」。C04-00 图 1 给的答案是「没有统一答案」——四种拓扑并存:xy 层持状态而 vc 层再持一份、xy 层零状态而 vc 层独占、Group 持状态而子项只读渲染期派生值、控件自身单文件闭环。选型表里每一条「而不是另一个」,根子都在这四种拓扑的差别上,而不在 props 表上(C04-00 图 1)。
状态流向是两段式的。第一段是写入守卫:手势先过一道「外部值有没有接管」的判定,本类左半边有三种互不等价的写法——=== undefined 把 null 判成受控、?? 把 null 判成非受控、in props 只看绑定与否不看值(C04-00 图 2、论点 1)。第二段是回正或抑回声:只有会驱动原生表单元素的控件需要 DOM 回正,因为浏览器自己记住了用户敲进去的字符与点出来的勾;switch、segmented 这类纯 div 控件不需要,于是连这一手也没抄(C04-00 图 2)。这两段合起来解释了为什么同一个症状在本类会有两种相反的解释路径:原生化那半去查 DOM 对账,非原生化那半去查判定守卫。
本类与库内标准件的关系是一条硬事实:useMergedState 与 useState 在本类的命中数都是零,判定全部就地手写(C04-00 论点 1)。技术上成立的理由只有 DOM 回正那一条——「比对 DOM 真实值再强制重渲染」这种能力,hook 的返回面结构上给不出来(C04-00 论点 2、取舍一)。由此得到一个可以直接拿去路由的推论:input 系与 checkbox 系、radio 系不接 hook 是有正当理由的,其余控件不接 hook 只是又添一份手写副本,代价是那一个判定在本类裂成 9 种拼法,至少 4 组之间对 null、undefined、「绑定了但没值」三种输入的归类结果相反(C04-00 论点 1)。对照边界很干净:隔壁 Select 的内核值那一层用的正是 useMergedState(C05-01 取舍 4),所以「受控行为符不符合你的直觉」这件事,跨过 C04 与 C05 的类边界要重新问一遍。
对外契约面比内部实现更分裂,选型时应当先看它。v-model 的参数名在本类分四组:update:value 是主流、update:checked 属于布尔控件、update:modelValue 全类只有 ColorPicker 一处、update:visible 全类只有 Password 一处;同时有 6 个文件不写 emits 声明,而靠「把 onUpdate: 后缀声明成 prop」把监听器从 attrs 里摘走(C04-00 论点 4)。change 的载荷形状另有 5 种,其中最容易被复用错的是 Radio 家族——独立模式下 update:value 携带布尔,进了 Group 携带的才是选项值(C04-00 坑 3、C04-04 论点 4)。
与 Form 的唯一耦合点是 formItemContext.onFieldChange():9 个控件目录里 2 个完全没接,接了的那 7 个也都没有同值门槛(C04-00 论点 5)。这一条直接决定「控件放进 <xy-form-item> 能不能即时校验」,答案是 segmented、color-picker 两个能填不能验(C04-00 坑 7)。
最后一句给准备新增控件的人:不要照抄任何一个邻近控件——邻近控件的判定至少有一半对 null 的处理和你的预期相反,可选路径只剩「接标准件」与「完全自己手写判定与回流」两条,原有的 isControlled 样板已随 2026-10-02 移除的两个控件退出本类(C04-00 论点 1 结论段)。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
| 收一段自由文本,值就是字符串,形态靠前后缀拼 | C04-01 input | C04-02 input-number | Input 的组件层零状态、只产出四个类名 prop,受控判定住在 vc 层(C04-01 论点 1);夹取、精度、格式化往返这一整套数值状态机在 Input 上一条都不存在(C04-02 论点 4、论点 6) |
| 多行、要自动高度 | C04-01 textarea | C04-01 input | TextArea 不经过 vc 层那个零状态的 Input,而是在 xy 层另持一份 stateValue 与一份 mergedValue,判定走 ?? 系,于是「父组件把值置为 null」在两者上得到相反的状态归属(C04-00 取舍二、拼法 2 与拼法 3);它的回流守卫还把优先级写坏了(C04-00 坑 2) |
| 搜索框:回车或按钮触发一次查询 | C04-01 search | C04-01 input 外加一个 Button | Search 不是「Input 旁边加按钮」,而是把按钮折进 addonAfter 借用 addon 形态,搜索值取的是基座内部那份 stateValue 而不是调用方的 model(C04-01 论点 3);带 allow-clear 时点清除图标会发出一次空查询(C04-01 坑 3) |
| 明文与密文可切换 | C04-01 password | 用 C04-05 switch 去控制显示 | Password 是本类唯一同时受 value 与 visible 两个 prop 管辖的控件(C04-00 论点 4),但 visible 漏写冒号会直接渲染成明文而用例照样通过(C04-01 论点 4、坑 6) |
| 数值输入:步进、精度、千分位 | C04-02 input-number | C04-07 slider | InputNumber 不站在输入基座上,自带原生 input、composition 与光标恢复,为的是每键都走一遍 parser 与 formatter(C04-02 论点 1、I04 论点 5);Slider 的真状态只在厂商层、像素与百分比按写死的量程互相换算,给的是量程手势不是精确定值(C04-07 论点 1、论点 6) |
| 布尔开关,但值的载体是自定义枚举(on、off、1、0) | C04-05 switch | C04-03 checkbox | Switch 用 checkedValue 与 unCheckedValue 做严格相等判开合,值域可以换但必须配齐,配错是静默显示为关(C04-05 坑 2);Checkbox 的 checked 是原生布尔、值所有权整个交给浏览器(C04-03 取舍一) |
| 多选、模型要的是数组 | C04-03 checkbox-group | C05-01 select 的 tags 模式 | Checkbox.Group 的数组真相在挂载注册表里,emit 之前还要过一遍 filter 与 sort(C04-03 论点 5),代价是 v-if 摘掉子项会顺手把它的值删掉(C04-03 坑 3);Select 走的是选项数据管道加三条互不调用的提交通道(C05-01 论点 3),选项多、要搜索、要虚拟化才值得付这笔钱 |
| 单选、选项固定两到四个、希望「永远显示父组件的值」 | C04-06 segmented | C04-04 radio-group | Segmented 把 value 声明成必填,是本类唯一没有非受控模式的控件,判定与类名都只读 props(C04-06 论点 4、C04-00 取舍四);Radio.Group 自持 stateValue、按键存在性判受控,而且整族没有 defaultValue(C04-04 论点 3) |
| 评分、要半星 | C04-08 rate | 无(本类唯一评分控件) | 它的 value 带默认值 0 使受控守卫不可达(C04-00 论点 1 拼法 9、坑 1),且半星只在鼠标路径可达、Enter 会塌成整星(C04-08 论点 2、坑 2) |
| 取色,且要连续拖拽取值 | C04-10 color-picker | 本类其余任何一个 | 它是本类唯一走 modelValue 通道的控件,也是唯一自带面板与定位、不复用触发器基座的(C04-00 论点 4、C04-10 取舍一);注意它的非受控初值通道整条失效,只传 defaultValue 不起作用(C04-10 论点 3、坑 5) |
| 拿不准该留在本类还是去 C05 | 有选项归一、搜索过滤、下拉定位就去 C05-01 | 只有「一个值加一次手势」就留在本类 | C05 整类只回答「调用方给的选项数据到下拉里能点的那些东西」这一条路(C05-00 §一);本类没有一个控件持有选项管道,最接近的也是把选项对象直接展开成 props(C04-06 论点 6) |
口径更新(2026-09-29):C04-06 原先只有 §四 的论点、§六 与 §七 是空壳,现已补完——§六 有 7 条取舍、§七 有 12 条缺陷族筛查加 10 条带五要素的坑(并真数了测试面:2 条用例 3 条断言,朴素 grep -c "it(" 在实现文件上得 3 全是 emit( 的假阳性)。所以凡涉及 Segmented,优先引 C04-06 的坑号,论点号只作机制出处。
甲 · 值写不回去,或者状态被冻住
@change 每次都在发 → C04-05 坑 1:内部状态只有一个写入者,而它在回流 watcher 里,非受控时永不触发(C04-05 论点 1)。同族还有 C04-00 坑 1(Rate 因 value 带默认值而恒受控)。C04-02 坑 4 与 C04-07 坑 2:这两条是受控语义本身而不是缺陷,缺的是注释,误判成本极高。undefined,到底丢不丢值? 本类有两种答案 → C04-00 坑 6:Switch 无守卫直接写入 undefined,而 Password 的 visible 通道做了二次守卫、保留最后一次内部值(C04-00 论点 4)。defaultChecked 重新初始化 → C04-05 坑 1 与 C04-00 坑 6:Switch 的这份初值住在 attrs 而不是 props,父组件改它不触发任何 watcher(C04-00 论点 1 拼法 4)。乙 · null 与 undefined 的归属翻转
null,Input 判受控、InputNumber 判非受控 → C04-00 论点 1 拼法 1 与拼法 3;同一个 input-number 自己还内外分裂(C04-00 取舍二)。C04-00 坑 2:in 的优先级高于 ||,右值变成恒真的空对象,且 vnode 没有 props 时直接抛 TypeError。hasProp 当成「prop 是否存在」来修 → C04-00 坑 8:它的实现是「不为 undefined」,改它会连带改掉 Slider 的受控语义(C04-07 论点 1)。defaultValue 的控件根本不起作用 → C04-10 坑 5:判定写反加目标变量只写不读,双重死亡。此处以 C04-10 为准,C04-00 论点 1 拼法 7 已按其反证更正过。丙 · Group 与子项:谁拥有值
v-model:checked 无效 → C04-00 坑 4、C04-03 坑 2、C04-04 坑 3:checked 在组内是渲染期派生比较值;Checkbox 侧还有 skipGroup 可以退回独立态,Radio 侧连逃生口都没有。skipGroup 从 false 改成 true,旧值还赖在数组里 → C04-03 坑 4:它只拦注册、不拦撤销。C04-03 坑 3;新增一条写注册表的路径却忘了 bump 信号,同样静默丢值 → C04-00 坑 5(C04-03 论点 5、I13 论点 5)。options 与默认插槽同时给,插槽整段消失 → C04-03 坑 10 与 C04-04 坑 4,两处是同一形状。@update:value 处理函数在 Radio 与 RadioGroup 间复用,一边收布尔一边收选项值 → C04-00 坑 3(C04-04 论点 4)。丁 · 进了 Form 就出问题
trigger: 'change' 的规则点了不跑 → C04-00 坑 7(segmented、color-picker 两个目录完全没接表单上下文,接了也没有同值门槛)。trigger: 'blur' 对半数控件静默失效 → C04-00 坑 7:全类只有四处发 blur 通知,checkbox、radio、switch、slider 都不在其中。C04-03 坑 11:那个 id 被挂到了不可聚焦的 div 上,而它给出的两条修法都会动到 props 直通链。C04-02 坑 3:夹取被打字状态门控,输入过程中放行越界值并照常发 change(C04-02 论点 4)。status="" 想清掉校验态,红框照旧 → C04-01 坑 4(C04-01 论点 2);隔壁 Select 同形 → C05-01 坑 3。C04-02 坑 7 是本类最重样本:修前一次 focus 把 handler 打 6 次、一次输入打 4 次,2026-09-29 已修为 1。同族的 Switch @change/@click、Radio @focus/@click、Slider @change/@blur 修前各 2 次,一并修完。判别只靠数调用次数(toHaveBeenCalledWith 照不出来),回归见 packages/xiaoye-ui/src/_util/__tests__/event-dispatch-count.test.tsx(23 条);三形态的全库口径见总导览「事件双投递」那条。戊 · 前缀、样式表与主题 Token
C04-03 坑 5:Group 把带后缀的前缀喂给了样式 hook,同一份样式注册成两张表(C04-03 论点 6);同族正确写法见 C04-04 论点 1。C04-04 坑 1:按钮形态会整体退化成无框文本。C04-10 坑 8:类名根本没走前缀拼接,同族论证在 C03-09 论点 6。theme.components 里配了组件 Token 却毫无效果 → C04-04 坑 6(声明未填的死键)、C04-05 取舍三(装配了却没人读的死槽位)。size 写覆盖样式命中空规则 → C04-05 坑 7、C04-06 坑 6(middle 静默落到默认尺寸)。direction 或 size 这类「本应由注入链兜底」的 prop 填默认值,全局配置被静默吃掉 → C04-08 坑 1(Rate 的 rtl 零效果,C04-08 论点 1);同形另见 C11-01 论点 5。己 · 属性、事件与可访问性的静默丢失
C04-10 坑 9:声明了不透传却没解构 attrs;C04-03 坑 1 是另一种形状——属性穿不透两道白名单(C04-03 论点 2)。C04-04 论点 5。C04-01 坑 7。C04-08 坑 4(C04-08 论点 4);aria-checked 是任意字符串且外部修不了 → C04-05 坑 4;选项各自成组、无 role → C04-06 论点 6。autofocus 不聚焦,但测试全绿 → C04-01 坑 8 与 C04-08 坑 7:共享用例自己造事件,因此测不出这条缺陷。isGroup 是公开但无实现的 prop → C04-03 坑 6 与 C04-04 坑 2,同族两篇各命中一次。e.target 是合成对象,类型在撒谎 → C04-03 坑 8:多出来的四个键读出来全是 undefined 而不报错。C04-05 坑 3 与 C04-05 坑 6:守卫只认 disabled,波纹抑制又依赖同一行,改一处必须成对改。C04-10 坑 7 与 C04-10 坑 6:前者按设备能力而非输入方式判关闭条件,后者申请与回收各判一次同一个可变 prop,账本本身的缺陷见 I10 论点 4。range 的整轨拖拽一拖就崩、拖拽中的柄抬不到最上层 → C04-07 坑 1 与 C04-07 坑 3:前者调一个不存在的方法,后者是类名键位与 CSS 选择器错位(C04-07 论点 5)。C04-03 坑 9(零断言用例)、C04-04 坑 7(恒真断言与被注释的回归,其中「靠 $refs 写内部 state 是否生效」该篇自己留了未证实标注,不要把它当回归网)、C04-08 坑 7。C04-00 论点 1(九种拼法的完整枚举与四种零状态)与 C04-00 论点 2、取舍一(唯一技术上成立的不接理由),再读 I13 论点 2 与 I13 坑 1 弄清标准件的边界;顺序不能反,否则会顺手清掉 DOM 回正那半能力。C04-01 论点 1、C04-01 坑 1、C04-01 坑 2,加上 I04 论点 5(形态决定哪个盒子承重)。组件层与结构层的判定口径不同,属于跨层修改,改一边就翻转。C04-00 坑 2 与 C04-00 取舍二——它是本类里少见的「修法有两条且都能落地」的坑,且现有用例只挂载、不覆盖回流分支。C04-02 论点 4、C04-02 论点 6、C04-02 坑 6;这三处任一动都会同时影响显示串、change 出参类型与夹取时机三条链(C04-02 论点 2 给出类型面与实现面的分歧点)。C04-03 论点 5、C04-04 论点 3、C04-00 论点 3、C04-00 坑 5。两个 Group 各修了对方不修的洞,任何单侧修改都会让差异更大(C04-03 取舍二 已把这笔结构性技术债定性)。C04-05 论点 4(Switch 绕过注入链自己拼的那一份)、C04-04 坑 5(同一个 key 注入三次)、C04-03 论点 3(三种合流路径与两种算子)。|| 与 ?? 在这里不等价,判形要一起看——Switch 那一份还额外挂着一个 attrs 形状的受控判定(C04-00 论点 1 拼法 4)。C04-00 论点 5、C04-06 论点 4(Segmented 的事件接线只在运行时成立)、C04-03 坑 11。未接入的两个目录要分别评估修法,别一次铺开。C04-03 论点 6、C04-04 论点 1、C04-10 坑 8;引擎侧后果归 A04,Token 分层归 A03,本类只负责指出哪一行喂错了值。C04-10 论点 4、C04-10 坑 6、C12-06 论点 1、I10 论点 4。该篇 取舍四 已确认它没有绕过 SSR 门闸。C04-00 §七 的安全边界段(三个 vc 底座合计零测试文件,收敛判定之前必须先补底座用例)、C04-03 坑 9、C04-04 坑 7、C04-07 坑 1(draggableTrack 那条死路正因为在零测试的底座里才活了这么久)。数测试文件必须同时看 .js、.ts、.tsx 三种后缀,只看一种会得出「这个控件没有测试」的错误结论(C04-08 坑 7、C04-00 测试现状)。