解决中文输入法导致的频繁 Input 事件!

502 阅读2分钟

大家好,我是前端架构师,关注微信公众号【程序员大卫】:

  • 回复 [面试] :免费领取“前端面试大全2025(Vue,React等)”
  • 回复 [架构师] :免费领取“前端精品架构师资料”
  • 回复 [书] :免费领取“前端精品电子书”
  • 回复 [软件] :免费领取“Window和Mac精品安装软件”

背景

在使用 input 输入框时,如果正在通过中文输入法打字,会出现一个问题:输入还未完成(比如拼音还没选字、还没按下回车)时,input 事件就已经触发了。 这会导致我们在还没真正确认输入内容时,数据就被提前更新了。

示例代码如下:

<script setup lang="ts">
const onInput = (e: Event) => {
	const value = (e.target as HTMLInputElement).value;
	console.log(value);
};
</script>

<template>
	<input @input="onInput" />
</template>

在上图的场景中,我们输入中文时,明明还没确认文字,但控制台已经打印了输入内容。

解决方案

要解决这个问题,可以利用 compositionstartcompositionend 事件:

  • compositionstart:中文输入法开始输入时触发
  • compositionend:中文输入法输入结束(确认文字)时触发

因此,我们可以在输入法开始时设置一个标记(flag),在输入完成前不触发 input 逻辑;等输入结束时,再统一更新一次值。

Vue3 封装组件示例

下面演示一个在 Vue3 中封装的输入组件。

说明:这里的 update:modelValue 是为了保证组件能正常配合 v-model 使用。modelValue 的类型为 String | Number,参考了 Element-Plus 的实现。

<script setup lang="ts">
import { ref, useAttrs } from "vue";

const props = defineProps({
	modelValue: {
		type: [String, Number],
	},
});

defineOptions({
	inheritAttrs: false,
});

const emit = defineEmits<{
	compositionstart: [e: CompositionEvent];
	compositionend: [e: CompositionEvent];
	input: [value: string | number];
	"update:modelValue": [value: string | number];
}>();

const attrs = useAttrs();
const isComposing = ref(false);

const updateValue = (e: CompositionEvent | Event) => {
	const value = (e.target as HTMLInputElement).value;
	emit("update:modelValue", value); // 保证 v-model 正常工作
	emit("input", value);
};

// 输入法开始
const onCompositionstart = (e: CompositionEvent) => {
	emit("compositionstart", e);
	isComposing.value = true;
};

// 输入法结束
const onCompositionend = (e: CompositionEvent) => {
	emit("compositionend", e);
	isComposing.value = false;
	updateValue(e);
};

// 非中文输入时触发
const onInput = (e: Event) => {
	if (isComposing.value) return;
	updateValue(e);
};
</script>

<template>
	<input
		v-bind="attrs"
		:value="props.modelValue"
		@compositionstart="onCompositionstart"
		@compositionend="onCompositionend"
		@input="onInput"
	/>
</template>

父组件调用方式(Parent.vue):

<script lang="ts" setup>
import InputIME from "./components/InputIME.vue";

const onInput = (value: string | number) => {
	console.log(value);
};
</script>

<template>
	<InputIME @input="onInput" />
</template>

Vue2 封装组件示例

在 Vue2 中,也可以采用类似的思路:

说明:通过 inheritAttrs: false 阻止默认事件透传,再利用 computed 中的 inputListeners 自定义事件绑定,手动管理输入法的 compositionstartcompositionendinput 事件。

<script>
export default {
	inheritAttrs: false,
	props: {
		value: [Number, String],
	},
	data() {
		return {
			isComposing: false,
		};
	},
	computed: {
		inputListeners() {
			return {
				...this.$listeners,
				input: (e) => {
					if (this.isComposing) return;
					this.updateValue(e);
				},
				compositionstart: (e) => {
					this.$emit("compositionstart", e);
					this.isComposing = true;
				},
				compositionend: (e) => {
					this.$emit("compositionend", e);
					this.isComposing = false;
					this.updateValue(e);
				},
			};
		},
	},
	methods: {
		updateValue(e) {
			const value = e.target.value;
			this.$emit("input", value);
		},
	},
};
</script>

<template>
	<input :value="value" v-bind="$attrs" v-on="inputListeners" />
</template>

父组件调用方式(Parent.vue):

<script>
import InputIME from "./components/InputIME.vue";

export default {
	components: {
		InputIME,
	},
	methods: {
		onInput(value) {
			console.log(value);
		},
	},
};
</script>

<template>
	<InputIME @input="onInput" />
</template>

总结

无论是在 Vue3 还是 Vue2 中,都可以通过封装一个通用的输入组件来解决这一问题,从而避免中文输入过程中出现的“值被提前更新”的情况。这样既能保持输入体验的流畅性,也能确保数据的准确性。