Bug report(问题描述)

当用户给一个元素(如按钮元素)添加v-waves指令后,如果用户再给元素添加一个click事件句柄,而且这个句柄在执行时触发了组件的更新,那么按钮的“波动”效果就会失效。

Steps to reproduce(问题复现步骤)

我用 vue-element-admin 项目 master 分支代码中的 src/views/table/complex-table.vue 页面中的一个search按钮作为示例,因为此处就有这个bug

  1. 给按钮元素添加v-waves指令:
<el-button v-waves class="filter-item" type="primary" icon="el-icon-search" @click="handleFilter">
  Search
</el-button>
  1. 给按钮元素添加一个click事件句柄handleFilter
handleFilter() {
  this.listQuery.page = 1
  this.getList()
},

getList() {
  this.listLoading = true // 触发更新
  fetchList(this.listQuery).then(response => {
    this.list = response.data.items
    this.total = response.data.total
    // Just to simulate the time of the request
    setTimeout(() => {
      this.listLoading = false
    }, 1.5 * 1000)
  })
}

其中 this.listLoading = true 语句触发了页面组件的更新。

  1. 页面组件的更新触发了v-waves指令的update hook:
    Vue文档对自定义指令的钩子函数的描述如下:

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

文档说当所在组件的 VNode 更新时调用自定义指令的update钩子,而我所遇到的情况有所不同。
this.listLoading = true 语句触发的是页面组件的更新,按理说el-button组件没有资格更新,但事实是v-waves指令的update钩子确实被触发了。

忽略这件事情,我们来看v-waves指令的update钩子的定义:

update(el, binding) {
  el.removeEventListener('click', el[context].removeHandle, false)
  el.addEventListener('click', handleClick(el, binding), false)
},

如上所示,第一句删除了按钮元素之前添加的click事件监听器,从而导致事件监听器从始至终都没有执行,“波动”效果当然也不会出现了。

Screenshot or Gif(截图或动态图)

GIF

其中第一次点击未出现“波动”效果,因为它触发了页面更新;而第二次点击没有触发更新,所以出现了“波动”效果。

Link to minimal reproduction(最小可在线还原demo)

vue-element-admin 项目 master 分支中的 src/views/table/complex-table.vue 页面中的 Search 按钮。

My Suggestion

回顾一下发生bug的整个过程:

  1. 用户触发按钮的点击事件。click事件是一个宏任务源,它发布了两个宏任务:@clickhandler和 v-waves指令的handler
  2. @clickhandler先执行,而它触发了页面组件的更新。页面组件的更新是一个微任务源,它发布了一个更新组件的微任务。由于微任务会在事件循环的末尾执行完,所以会先进行组件的更新,然后才执行v-waves指令的click handler
  3. 页面组件更新时触发了v-waves指令的update钩子,在update钩子中删除了之前添加的click事件监听器。

所以,我们只要先执行v-waves指令的click handler,再删除click事件监听器就行了。

一个简单的方法是延迟到一个宏任务中删除监听器:

update(el, binding) {
  const removeHandler = el[context].removeHandle
  setTimeout(_ => {
    el.removeEventListener('click', removeHandler, false)
  })
  el.addEventListener('click', handleClick(el, binding), false)
},

转载自:https://github.com/panjiachen/vue-element-admin/issues/2785 

Logo

前往低代码交流专区

更多推荐