Vue `<component>` 踩坑笔记

0 阅读2分钟

Vue <component> 笔记


1. 正常用法

<component :is="tag" value="hello" />

tag = 'input' 时渲染成:

<input value="hello">

<component> 是 Vue 的内置特殊元素,编译期会变成:

_resolveDynamicComponent(is)

is 的具体值运行时才解析。


2. 坑一:is 藏进 v-bind 不生效

错误写法

<component v-bind="{ is: 'input', value: 'hello' }" />

现象

控制台报错:

[Vue warn]: Failed to resolve component: component

DOM 里出现死标签:

<component is="input" value="hello"></component>

原因

is 必须显式写在 <component> 标签上。

藏进 v-bind 对象后,编译器不会把它当动态组件的 is,而是当普通 attribute。 <component> 拿不到 is,就被当普通组件名解析,找不到就回退成未知元素。

正确写法

<component :is="'input'" v-bind="{ value: 'hello' }" />

静态也行:

<component is="input" v-bind="{ value: 'hello' }" />

编译产物对比

显式写 is

_createBlock(_resolveDynamicComponent('input'), ...)

藏进 v-bind

const _component_component = _resolveComponent("component")

_createVNode(_component_component, _normalizeProps(_guardReactiveProps({
  is: 'input',
  value: 'hello'
})), null, 16)

编译器完全没识别出动态组件,而是把 <component> 当普通组件名去解析。


3. 坑二:v-model<component> 上,当 is 是原生元素时失效

例子

<component :is="'input'" v-model="username" />

能打字,但 username 不变。

为什么失效

v-model 是编译期语法糖。编译器看到它,要决定展开成什么,但它只看标签名

看到标签是 <component>,编译器不知道运行时 is 会解析成原生元素还是组件,只能默认按“组件”展开:

_createBlock(_resolveDynamicComponent('input'), {
  modelValue: username,
  "onUpdate:modelValue": $event => (username = $event)
}, null, 8, ["modelValue"])

这是组件那套约定:传 modelValue,监听 update:modelValue

运行时 is 解析成原生 input。原生 input 不认识 modelValue 这个 prop,也永远不会触发 update:modelValue 事件。结果就是:

  • 能输入 —— 那是浏览器自己的行为
  • username 不同步 —— 组件那套约定,原生元素接不住

对比:为什么 <input v-model> 就正常

直接写 <input v-model="username" />,编译器知道这是原生元素,会按原生那套展开,内部直接操作 DOM 的 valueinput 事件。所以正常。

关键区别

写法编译后走哪条路
<component v-model>展开成 modelValue + update:modelValue组件路径
<input v-model>展开成原生 v-model 指令原生元素路径

编译器只看标签名,不看 is 的值。

那想用 v-model 怎么办

三条路,按场景选:

① 让 is 指向真组件

真组件认 modelValue 那套,v-model 就通了:

<component :is="RealComponent" v-model="x" />

② 不用 v-model,手动接

<component
  :is="'input'"
  :value="x"
  @input="e => x = e.target.value"
/>

取值按元素类型:input / selecte.target.valuecheckboxe.target.checked

③ 干脆不用 <component>,直接 v-if / v-else

<input v-if="type === 'input'" v-model="x" />
<textarea v-else v-model="x" />

如果只是 input / textarea 二选一,这是最稳的。


4. 一句话总结

  • is 必须显式写在 <component> 标签上,不能藏进 v-bind 对象。
  • v-model 认标签,不认 is。标签是 <component>,它就按组件路径展开;运行时若是原生元素,接不住。
  • 动态原生标签 + v-model 天生走不通,要么让 is 指向真组件,要么手动绑定,要么直接 v-if / v-else