介绍
v-model
可以在组件上使用以实现双向绑定, v-model
本质上是语法糖,经过模版编译器后会对 v-model
转换为:
上面是表单元素 v-model
的转换,而自定义组件会转换为:
那么在自定义组件 <CustomInput>
中实现 v-model
需要:
- 获取 prop 中的
modelValue
- 值改变需要触发
update:modelValue
自定义事件
为了方便开发,VueUse 提供了 useVModel
来实现自定义组件的 v-model
。
useVModel
v-model
的封装实现
使用
配置项
源码
watch
和 computed
方式实现 v-model
源码中 passive
为 true
采用了 watch
的方式实现 v-model
,false 采用 computed
,两者有什么区别呢?
watch
实现方式:
这种方式是在组件内部声明了一个 currentValue
变量,通过监听 props.modelValue
赋值给 currentValue
变量,currentValue
值改变触发 update:modelValue
自定义事件。这种方式的优点组件内部有个自己的状态,不会受父组件的传值影响。但相对 computed
的方式复杂了一点。
props.modelValue
值改变时会触发监听执行 currentValue.value
值改变又会触发 currentValue
的监听函数,但不需要触发update:modelValue
,因此需要添加 if (val === props.modelValue) return
。
这里我们可以将 watch
替换成 watchEffect
,让代码更简洁:
这里需要注意的是 currentValue
的监听不能换成 watchEffect
,因为换了之后就是 props.modelValue
和 currentValue
其中一个值改变就会触发执行。
代码可以进一步优化:不用 v-model
绑定 currentValue
:
用 :value
和 @input
可以更加灵活的在触发 update:modelValue
之前做一些逻辑处理。也避免了 currentValue
的监听多次触发的情况。
computed
的实现方式:
这种方式相对简单,但有个问题,如果引用该组件时没有绑定 v-model
时,组件中 input 的 v-model
也就会失效。在依赖v-model
绑定的变量做一些处理时会受影响。
第二个问题是如果 v-model
绑定的是一个对象,当对象中的属性值改变时不会触发 computed
的 set 方法:
上面例子对象的属性值也会随着输入的内容而改变,不过这个改变的原因的不是因为执行了 set
方法,而是因为对象是引用类型。我们在实际开发中会尽量避免这种情况,因为这样会导致组件间的数据流变复杂,很难跟踪位数据,因此开发中尽量保持单向数据传递。有两种方式解决:
一是为 modelValue
对象的每个属性都声明一个 computed
,这种就比较繁琐。
还有一种方式是 get
方法返回一个 Proxy
对象
这种方式每次执行 get 方法会重新实例化一个 Proxy
对象,因此就不会有上面对象引用而修改了父级组件传来的对象。并且 Proxy
对象属性值改变时会触发 set
方法而执行 emits("update:modelValue")
,从而实现双向绑定。但个人认为这不是一个好的方案:
第一:这里并没有兼容 modelValue
是值类型的时候,也就说在 computed get
方法中要加个判断处理值类型和对象两个分支逻辑。
第二:computed get
方法每次都会新建一个 Proxy
对象,当然这个对性能的影响很小。
第三:Proxy
并不是一点缺点都没有的,在数组的处理上还是有很多需要注意的地方。
总结
v-model
的实现可以采用 watch
和 computed
两种方式,computed
的方式更简单,在特定场景下会有问题,大部分场景不会有问题,因此 VueUse 默认采用 computed
的方式。