前言
有个挺常见的需求相信大家应该都遇到过,就是一个搜索框,边输入边提示,类似于下面这样:

这玩意在前端也挺好实现的,就 v-model 然后 watch 再做个防抖请求接口呗!于是我:
<template>
<input v-model="value">
</template>
<script setup>
import { ref, watch } from 'vue';
const value = ref('')
watch(value, value => 请求接口(value))
</script>
这里先省略掉防抖,因为希望代码看起来更简洁、更让人专注 bug 所在位置、减少干扰,大家能明白意思就行。为了更好的向大家展示这个 bug 我们把请求接口换成更加经典的 console.log,每打印一次就代表请求了一次接口:
<template>
<input v-model="value">
</template>
<script setup>
import { ref, watch } from 'vue';
const value = ref('')
watch(value, value => console.log(value))
</script>

只要输入框里的值发生变化了,那么就会请求接口,就会像下面这样:

我们想搜索黑粉,结果刚打出第一个字母就已经开始替我们搜索了,随着字符越输越多,黑粉也慢慢呈现在了我们的面前。
中文输入法
但就在产品走查的时候,她们用中文输入法输入的时候却发现没有随着字符的输入而进行搜索:

可以看到我们输入了这么多字符,但控制台从始至终都没打印过,证明了根本就没有监听到输入框里的变化。刚开始的时候我还狡辩呢,说什么像这种带着下划线的代表没输入完不会进入到输入里,所以输入框检测不到任何字符:

她听完后半信半疑的走了,而我自己则是全信了,因为我以为事实就是这样。但过了一会她又回来了:你看百度谷歌它们的中文输入法都是没问题的啊:

这下轮到我尴尬了:她会不会以为刚刚是我找了个蹩脚的理由敷衍她,但实际上我真这么认为的,因为我看了半天代码也没看出来哪写的不对,但她用事实告诉我这就是我的 bug。想了半天也没想出个所以然来,后来琢磨会不会是 Vue 的 bug?因为无论百度谷歌还是必应它们都没用 Vue,所以它们没这个 bug,于是我用原生 JS 试了一下:
<input>
document.querySelector('input').oninput = ({
target: { value }
}) => console.log(value)

看来果然是 Vue 的锅,然后我赶忙跟她解释说这是 Vue 的 bug,她一脸鄙视:

你都赖多少次
Vue了,是不是只要一有bug就都甩到Vue身上?
人家Vue好歹也是三大框架之一,不至于这么Low吧?一个带输入法的输入框都不支持?
我:这是真的啊😭,不过也不是不能解决,这次我把原生的写法写到 Vue 里去:
<template>
<input ref="input">
</template>
<script setup>
import { ref, onMounted } from 'vue';
const input = ref('');
onMounted(() => input.value.oninput = ({ target: { value } }) => console.log(value))
</script>
<style>
html, body {
background: #222;
}
input {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 300px;
height: 30px;
padding: 0 10px;
border: 1px solid #395e71;
border-radius: 16px;
color: antiquewhite;
background: none;
outline: none;
}
</style>
结果居然是好的:

接下来我陷入了短暂的沉思:原来的代码在刨除掉业务部分后在 <input> 上仅仅只用了 v-model 这一个属性,难道是 v-model 的问题?为了验证我的猜想我把原先的 v-model 给改成了 :value + @input:
<template>
<input :value="value" @input="value = $event.target.value">
</template>
<script setup>
import { ref, watch } from 'vue';
const value = ref('')
watch(value, value => console.log(value))
</script>

这直接惊掉了我的下巴,因为在我印象中 v-model 就是 :value + @input 的语法糖,它俩是全等的关系:
<input v-model="value"> === <input :value="value" @input="value = $event.target.value">
因为 Vue 官网里就是这么写的:

但为什么它们俩的行为不一致呢?而且为什么 v-model 会在中文输入法的情况下出 bug 呢?这些问题我们只能从源码里来找答案了。
源码
源码版本 3.3.4,在 packages 里的 runtime-dom 下的 src 内的 directives 中有个 vModel.ts:

在第 43 行有一个 vModelText,从名字上也可以看出来这是专门针对 <input> 的,因为只有这个元素是可以输入 text 的,其它以 vModel 开头的还有:
vModelCheckboxvModelRadiovModelSelectvModelDynamic
但咱们想搞清楚的是中文输入法 bug,所以只看 vModelText 就行,其它的不用管。简单的扫了一眼,直觉告诉我问题出在了第 51 行:

因为从命名上来看,.composing 是正在组合中的意思,我们用中文输入法打字的时候不就是正在组合中么:

正在组合中就 return,后面的代码也就不执行了,这也非常符合我们目前所遇到的状况。不过 e.target 上有 .composing 这个属性么?我们用原生来试一下:
document.querySelector('input').oninput = ({
target: { composing }
}) => console.log('composing: ', composing)

可以看到根本就没这个属性,我就说嘛!我怎么不记得还有这个属性,那这个属性肯定是 Vue 自己添加上去的。我们继续来看源码,在第 67、68 行我发现了这个:

然后找到 onCompositionStart 和 onCompositionEnd 这两个函数:

原来还真是 Vue 自己加上去的属性,不过在这之前我从来没听说过 compositionstart 和 compositionend 这两个事件,盲猜肯定是在中文输入法(其实这么说不太准确,准确来说应该是所有需要把英文字母的组合转换成另外一种文字的输入法)输入的时候会触发的事件,我们来试一下:
document.querySelector('input').addEventListener('compositionstart', e => console.log(e))

可以看到我们用英文打字时并没有打印出任何东西来,这代表了没有触发事件,当换成中文输入法时就能触发事件并打印出 CompositionEvent 了。原本我以为这是个 bug,但看完源码后我意识到这是有意为之,但我觉得这种有意为之并不好,因为官网上明明说了 v-model === :value + @input,不过在中文输入法下表现却并不一致。当然我知道这也是出于好意,尤雨溪可能觉得中文输入法打字时产生的字母并没有什么有效信息,比方说我们在搜索框里搜一个鼠头:

我们获取到的值是 shu tou,但这个 shu tou 并不是个没用的信息,用它照样可以显示出当前的热点搜索:

然后随便一点击,就能看到一些:

而且还有一个可能,就是本来就是想用英文搜,只不过是忘记了切换输入法:

我们的产品就属于这种,我们做的是海外业务,用的是英文,但产品忘记了切换输入法,结果就出现了这个 bug。
改进
他加上 .composing 的判断自然有他的道理,但大家仔细想想,如果有人遇到了和我相同的 bug,去看官网:

那他会不会想到把 v-model 换成 :value + @input 就能够解决掉这个 bug?那不就是个简写么?意思不都是一样的么?谁能想得到啊家人们:

但也不代表 .composing 没有意义,不过我觉得 com 不 composing 的控制权应该交给开发者,v-model 不是有各种修饰符么:

可以搞个内置修饰符 .composing:
<input v-model.composing="value">
不过我觉得尤雨溪可能觉得这样写比较麻烦所以才做了 v-model 默认就有 .composing 的判断,那这样也可以搞个 .nocomposing 修饰符嘛:
<input v-model.nocomposing="value">
然后再在官网上标明 v-model 其实在中文输入法正在输入的情况下与 :value + @input 的表现并不一致,如果想要一致的话就加上 .nocomposing 修饰符:
<input v-model.nocomposing="value">
结语
大家可以去提个 PR,这可是一个成为 Vue Contributor 的好机会。如果尤雨溪不明白为什么要这么改时,你可以把我这篇文章的链接发给他看,相信他就明白了 v-model 有可能会给大家带来的困境了。



















