taro3 输入框 onInput 处理输入字符串问题

taro: 3.3.10
vue: 3.2.16

Taro Inputopen in new window

如何在 onInput 中处理输入的字符串

背景: 一个输入框,需要在用户输入后将输入的字符串转换成大写字母和数字组成的字符串,字符限制11位。不符合条件时将输入框清空。

const state1 = reactive({
  formModel: {
    testStr: "",
  },
});

<input
  value={state1.formModel.testStr}
  type="text"
  placeholder=""
  clearable
  maxlength="11"
  onInput={handleStr}
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里需要在 handleStr 方法里面实现需求:
一开始是这么实现的,用正则匹配是否符合条件,符合则将字符转成大写,不符合则将双向绑定的值置空。如下

const handleStr = (e) => {
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  state1.formModel.testStr = value;
};
1
2
3
4
5
6
7
8
9
10

看起来很符合逻辑,试一下输入

能够成功地将输入的小写字母转成大写
接着我把输入法改成了中文,输入中文字符

这时候问题出现了,输入的中文字符没有被清空???

打印一下此时的 state,看看里面 formModel.testStr 的值

testStr 的值为"",也就是说输入的中文字符在页面上存在,而双向绑定中的数据为空

回顾一下处理逻辑,当不符合正则时,把值置为"",乍一看很合理,仔细一看,问题出现在最后赋值的时候

state1.formModel.testStr = value;
1

当输入中文字符时,这里赋值的 value 为 "",而 testStr 定义的初始值也为 ""。

const state1 = reactive({
  formModel: {
    testStr: "",
  },
});
1
2
3
4
5

推测是因为相同值没有触发页面重新渲染,类似的情况有时在 vue 的 watch 中也遇到过。
第一个想法是直接调用 forceUpdate,看下是否没有重新渲染导致的问题:

// vue3 的实例方法需要先引入 getCurrentInstance
import { reactive, getCurrentInstance } from "vue";
setup() {
  // setup 中执行获取实例 ctx
  const ctx = getCurrentInstance().ctx;

  const handleStr = (e) => {
    const currVal = e.detail.value;
    let value = "";
    if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
      value = currVal.toUpperCase();
    } else {
      value = "";
    }
    // 对绑定的对象属性赋值之后,调用 forceUpdate
    state1.formModel.testStr = value;
    ctx.$forceUpdate();
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

看看页面输入的情况

多次输入中文字符都可以清空了,但是需要引入实例再调用强制更新方法,有没有更好的方法?
其实这里主要是没有触发双向绑定的变量改变从而触发渲染函数执行,那么只要在赋值的时候能触发 setter 更新视图,就可以解决问题

const handleStr = (e) => {
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  // 对绑定的对象属性赋值之后,调用 forceUpdate
  // state1.formModel.testStr = value;
  // ctx.$forceUpdate();
  // 给 state1 中的 formModel 重新赋值,触发页面更新
  state1.formModel = Object.assign({}, state1.formModel, {
    testStr: value,
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

二次封装了 taro Input 组件产生的问题

日常开发使用的是基于 taro Input 组件二次封装的组件

cust-input.vue
省略了部分代码

<template>
  <view>
    <input
      v-bind="{ ...$attrs, class: inputClass }"
      :value="modelValue"
      @Input="handleInput"
    />
  </view>
</template>
<script>
setup(props, {emit}) {
  const handleInput = (e) => {
    console.log("inside onInput");
    const value = e.detail.value;
    emit("update:modelValue", value);
  };
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里可以看到在组件里面接收传入的一些属性和监听事件,外部使用组件的代码如下:

const state1 = reactive({
  formModel: {
    testStr: "",
  },
});

<cust-input
  v-model={state1.formModel.testStr}
  type="text"
  placeholder=""
  clearable
  maxlength="11"
  onInput={handleStr}
/>

const handleStr = (e) => {
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  state1.formModel = Object.assign({}, state1.formModel, {
    testStr: value,
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

与直接使用 taro 的 Input 组件类似,只不过在内部包了一层方便做自定义操作。
但在这个操作用户输入的字符串的需求场景下,这种用法就存在问题,看一下操作效果。

在 cust-input 组件上绑定的 onInput 看起来完全没有生效,是没有绑定成功吗?在 onInput 事件里面打印看看

const handleStr = (e) => {
  console.log("outside onInput");
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  state1.formModel = Object.assign({}, state1.formModel, {
    testStr: value,
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13

结果是可以正常打印出"outside onInput",cust-input 内部也有一个监听 input 事件,也打印看看

// cust-input.vue
const handleInput = (e) => {
  console.log("inside onInput");
  const value = e.detail.value;
  emit("update:modelValue", value);
};
1
2
3
4
5
6

同样可以正常打印,不过这时候发现一个情况,跟外部 onInput 的打印时序的问题。
当输入的时候,会先打印 outside onInput,再打印 inside onInput
原来是绑定事件的先后顺序的问题,外部使用组件的时候通过 $attrs 绑定,而组件内部的 input 组件随后也绑定了input事件

<input
  v-bind="{ ...$attrs, class: inputClass }"
  :value="modelValue"
  @Input="handleInput"
/>
1
2
3
4
5

外部 onInput 对输入字符串处理后赋值,之后内部 onInput 也对绑定的modelValue 进行更新,导致输入的值看起来没有变化

// 外部 onInput 先执行
const handleStr = (e) => {
  console.log("outside onInput");
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  // 改变了值,赋值
  state1.formModel = Object.assign({}, state1.formModel, {
    testStr: value,
  });
};

// 内部 onInput 后执行
const handleInput = (e) => {
  console.log("inside onInput");
  const value = e.detail.value;
  // 还是原来输入的值,赋值
  emit("update:modelValue", value);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

这种情况很容易就会想到,假如外部 onInput 里面的赋值操作延迟到内部的赋值操作之后,是不是就可以正常更改,经过尝试,有以下几种情况:

const handleStr = (e) => {
  console.log("outside onInput");
  const currVal = e.detail.value;
  let value = "";
  if (/^[a-zA-Z0-9\-]{0,11}$/.exec(currVal)) {
    value = currVal.toUpperCase();
  } else {
    value = "";
  }
  /**
   * 第一种方式,直接用 setTimeout 延迟执行
   * 在微信开发者工具中可以正常生效,但在真机上还是存在问题
   */
  setTimeout(() => {
    console.log("outside setTimeout");
    state1.formModel = Object.assign({}, state1.formModel, {
      testStr: value,
    });
  }, 0);

  /**
   * 第二种方式,先用 nextTick 再用 setTimeout 延迟执行
   * 这种方式可以正常生效
   */
  nextTick(() => {
    setTimeout(() => {
      state1.formModel = Object.assign({}, state1.formModel, {
        testStr: value,
      });
    }, 0);
  });
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

这里暂时解决问题,没有继续深入,包括使用 nextTick 嵌套 setTimeout 这种方式,微任务嵌套宏任务,是否会带来一些不好的影响,也没有过多深究,也许后续遇到再进行处理。