iview input 的 on-change 事件为什么需要 nextTick ?

2,353 阅读1分钟

问题

问题源自需求中的一个 bug:

项目使用 iview,需求是一个多行输入框,仅允许输入数字,可以换行,使用 input 组件:

<Input
    v-model="form.ids"
    type="textarea"
    :number="true"
    @on-change="handleChange"
/>

handleChangeLimit(event) {
    const value = event.target.value;
    if (value) {
        this.form.ids = value.replace(/[^\d\n]/g, "");
    }
},

一开始是这样的,在换行时确实能处理过滤非数字内容。但是在输入的过程中、以及 blur 时不会处理数据。例如:

image.png

nextTick

有点不可思议,首先 on-change 是在输入框“改变”的时候进行事件处理,为什么输入的过程中没有变化?

看起来只是 Vue 简单的数据绑定,为什么没有生效呢?

在请教同事之后,加上 nextTick 就好了:

<Input
    v-model="form.ids"
    type="textarea"
    :number="true"
    @on-change="handleChange"
/>

handleChangeLimit(event) {
    const value = event.target.value;
    if (value) {
        // 注意此处 nextTick
        this.$nextTick(() => {
            this.form.ids = value.replace(/[^\d\n]/g, "");
        });
    }
},

这是什么原因呢?虽然知道 nextTick 的作用,但是还是不理解,此处为什么需要 nextTick 呢?

Vue 官网深入响应式原理一节关于 nextTick 的介绍:

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用

按说我们不需要关注执行的循环队列,只是改变输入框的值而已啊,这也正是上面所说的。

此段可放在最后再看

实际上确实是这样(后面还会说到),问题在于这个需求的特殊,在处理数据的过程中,如果输入非数字,就会转换为空,也就是最终的结果和之前的结果是一样的,导致 Vue 不再渲染页面,所以看起来数据(this.form.ids)更新了,页面没有变化。

为了说明其中一些原理,我做了一些简单的尝试:

v-model 双向数据绑定

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input v-model="value" />
</template>

image.png

再正常不过的效果

v-model 语法糖的本质

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  methods: {
    handleInput(event) {
      const value = event.target.value;
      this.value = value;
    },
  }
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input :value="value" @input="handleInput" />
</template>

这样也很好理解。

输入框过滤非数字

现在加上我们的需求,限制输入为数字。

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  methods: {
    handleInput(event) {
      const value = event.target.value;
      this.value =value.replace(/[^0-9\.]+/g, '');
    },
  }
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input :value="value" @input="handleInput" />
</template>

image.png

有问题,改变 this.value 页面并没有跟着渲染。为什么双向绑定的值没有渲染页面?

输入框过滤非数字,使用 nextTick

还是加上 nextTick:

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  methods: {
    handleInput(event) {
      const value = event.target.value;
      this.$nextTick(() => {
        this.value =value.replace(/[^0-9\.]+/g, '');
      })
    },
  }
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input :value="value" @input="handleInput" />
</template>

然而,却不生效了?开头我们解决问题的方法,不就是用 nextTick 吗?此时怎么不行了呢?

输入框过滤非数字,注意双向绑定

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  methods: {
    handleInput(event) {
      const value = event.target.value;
      // 注意此处
      this.value = value;
      this.$nextTick(() => {
        this.value = value.replace(/[^0-9\.]+/g, '');
      })
    },
  }
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input :value="value" @input="handleInput" />
</template>

因为我们还没有进行双向绑定,这样在 nextTick 时,this.value 的值没有变化,没有变化就不会渲染页面。所以加上 this.value = value; 就好了。就是要先完成双向绑定,此时输入框的输入内容就会双向绑定到 this.value,比如 66anextTick 的作用是,在下一个事件循环中,再对 this.value 赋值,此时为 66,这样值不一样,就触发了渲染。这就是为什么需要 nextTick

输入框过滤非数字,使用 v-model 的双向绑定

<script>
export default {
  data() {
    return {
      value: "",
    };
  },
  methods: {
    handleChange(e) {
      const value = e.target.value;
      console.log("已经绑定 value:", this.value);
      this.value = value.replace(/[^0-9\.]+/g, "");
      console.log("处理后的 value:", this.value);
    },
  },
};
</script>

<template>
  <h1>{{ value }}</h1>
  <input v-model="value" @input="handleChange" />
</template>

为什么使用 v-model,就不需要 nextTick 了呢?

因为 v-model 已经完成了双向数据绑定,我们单独写了 input 事件处理需求,相当于此处有两个 input 事件,自己写的 input 事件执行时,v-model 的 input 事件已经绑定了值。

iview on-change 事件

回到最开始 bug 的解决,只是加了 nextTick,并没有赋值啊?

这个在 iview 源码中有解答:github.com/iview/iview…

<input
    :id="elementId"
    :autocomplete="autocomplete"
    :spellcheck="spellcheck"
    ref="input"
    :type="type"
    :class="inputClasses"
    :placeholder="placeholder"
    :disabled="disabled"
    :maxlength="maxlength"
    :readonly="readonly"
    :name="name"
    :value="currentValue"
    :number="number"
    :autofocus="autofocus"
    @keyup.enter="handleEnter"
    @keyup="handleKeyup"
    @keypress="handleKeypress"
    @keydown="handleKeydown"
    @focus="handleFocus"
    @blur="handleBlur"
    @compositionstart="handleComposition"
    @compositionupdate="handleComposition"
    @compositionend="handleComposition"
    @input="handleInput"
    @change="handleChange">

handleInput (event) {
    if (this.isOnComposition) return;
    let value = event.target.value;
    if (this.number && value !== '') value = Number.isNaN(Number(value)) ? value : Number(value);
    this.$emit('input', value);
    // 2)
    this.setCurrentValue(value);
    // 1)
    this.$emit('on-change', event);
},
handleChange (event) {
    this.$emit('on-input-change', event);
},
setCurrentValue (value) {
    if (value === this.currentValue) return;
    this.$nextTick(() => {
        this.resizeTextarea();
    });
    this.currentValue = value;
    if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
        this.dispatch('FormItem', 'on-form-change', value);
    }
},
  • 1): 原来 on-change 事件实际上封装的是 input 事件
  • 2): 在 input 事件中处理了双向数据绑定的问题

总结

  • v-model 绑定 value 和 input 事件
  • Vue DOM 异步更新,数据没有更新,wather 不会调用
  • nextTick 让我们对数据的处理在下一个事件循环生效,避免了 wather 被去重,只执行一次。

参考