日期选择组件el-date-picker, 通过源码看change监听事件为什么不触发的问题

3,373 阅读2分钟

问题概述:

最近在做Vue2升级Vue3的需求,发现一个el-date-picker日期选择组件失效了。选择日期后页面有回显,但不会触发change事件的回调函数,然而清除日期后又会触发change。神奇的是页面上有两个el-date-picker组件,另一个却正常。下面是代码:

<el-date-picker
  v-model="endTime"
  type="datetimerange"
  value-format="yyyy-MM-dd HH:mm:ss"
  :default-time="['00:00:00', '23:59:59']"
  @change="search"
>

网上搜索后得到如下的解决方法:将change事件改为input,获取input的value值来修改v-model的值。

<el-date-picker
  v-model="endTime"
  type="datetimerange"
  value-format="yyyy-MM-dd HH:mm:ss"
  :default-time="['00:00:00', '23:59:59']"
  size="mini"
  @input="searchWithEndTime"
>
searchWithEndTime(value) {
  console.log(this.endTime, value)
  this.endTime = value;
  this.search();
},

虽然input事件会触发,但打印v-model的值会得到null。

源码阅读

为了搞明白这个问题,找到了ElementUI中这个组件的源码。

<el-input
    v-bind="firstInputId"
    class="el-date-editor"
    :class="'el-date-editor--' + type"
    :readonly="!editable || readonly || type === 'dates' || type === 'week'"
    :disabled="pickerDisabled"
    :size="pickerSize"
    :name="name"
    v-if="!ranged"
    v-clickoutside="handleClose"
    :placeholder="placeholder"
    @focus="handleFocus"
    @keydown="handleKeydown"
    :value="displayValue"
    @input="(value) => (userInput = value)"
    @change="handleChange"
    @mouseenter="handleMouseEnter"
    @mouseleave="showClose = false"
    :validateEvent="false"
    ref="reference"
  >

其中值得注意的是displayValue这个值,这个值会展示在页面上,是一个computed

displayValue() {
  // ...
  // 此处省略
  if (Array.isArray(this.userInput)) {
    return [
      this.userInput[0] || (formattedValue && formattedValue[0]) || '',
      this.userInput[1] || (formattedValue && formattedValue[1]) || '',
    ]
  } else if (this.userInput !== null) {
    return this.userInput
  } else if (formattedValue) {
    return this.type === 'dates'
      ? formattedValue.join(', ')
      : formattedValue
  } else {
    return ''
  }
},

我们可以看到这个值主要与userInput有关,而前面我们看到userInput会在input事件触发后被赋值,所以结论是,页面上展示的值,是由我们点击组件选择日期后直接得到的。

再来看看什么情况下会触发change事件:

pickerVisible(val) {
  if (this.readonly || this.pickerDisabled) return
  if (val) {
    this.showPicker()
    this.valueOnOpen = Array.isArray(this.value)
      ? [...this.value]
      : this.value
  } else {
    this.hidePicker()
    this.emitChange(this.value)
    this.userInput = null
    if (this.validateEvent) {
      this.dispatch('ElFormItem', 'el.form.blur')
    }
    $emit(this, 'blur', this)
    this.blur()
  }
},

这是watch中监听的一个属性,在选择完日期点击确认后,会关闭picker,让这个值变为false,就会走else分支的代码。


emitChange(val) {
  // determine user real change only
  if (!valueEquals(val, this.valueOnOpen)) {
    $emit(this, 'change', val)
    this.valueOnOpen = val
    if (this.validateEvent) {
      this.dispatch('ElFormItem', 'el.form.change', val)
    }
  }
}

这是组件主动去触发change事件的方法,它需要先比较传入的val和valueOnOpen这个属性。前面我们看到传入的val就是this.value,在data中可以看到,value由modelValue决定,也就是我们为组件绑定的v-model值。

data() {
    return {
      value: this.modelValue??this.$attrs.value,
    }
},

而在清除数据时,会触发this.emitInput(null)

handleChange() {
  if (this.userInput) {
    const value = this.parseString(this.displayValue)
    if (value) {
      this.picker.value = value
      if (this.isValidValue(value)) {
        this.emitInput(value)
        this.userInput = null
      }
    }
  }
  if (this.userInput === '') {
    this.emitInput(null)
    this.emitChange(null)
    this.userInput = null
  }
},

从这里也能看出,为什么input事件一定会触发,而change有时不触发。

我们发现页面回显只与我们输入的值有关,而change事件与绑定的v-model有关。

所以问题应该出在页面有两个date-picker的问题上,经过实验,发现页面有多个date-picker时,只有第一个会触发change,v-model绑定一样的值也是这样。

总结

当页面上出现两个date-picker时,只有第一个会触发change事件,后面的组件需要监听input事件来获取输入值。这应该与Vue3的新特性有关,具体原因以后我会再分析。