Is your v-debounce package really okay? Custom anti-shake command to step on the pit record

origin

It was a leisurely morning when a message from the test suddenly caught my attention.
"How does your online button perform the offline operation?", I: "???",
quickly open the code to see, there is nothing wrong, the method of event binding is correct , the prompt is not wrong, what happened ?
It's no problem to run the project and try it. You can test me.
I ran to the test seat and started to line up. At first glance, there was really a problem, but I would have no problem myself.
Hurry up and download the online version, there is nothing wrong with it, can this be an occasional bug?
Until I clicked on another task, one of the two task buttons happened to be online and the other was offline. There was really a problem (→_→)
Test brother, I was wrong, don't mention my research.

Problem recurrence

The internal code is inconvenient to be released directly, so I wrote a simple demo, which is better to understand.

First a simple page structure

<template>
  <div class="container">
    <button v-debounce="handlerClick1" v-if="showbutton">1</button>
    <button v-debounce="handlerClick2" v-else>2</button>
    <button @click="toggle">toggle</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      showbutton: true,
    }
  },
  methods: {
    toggle() {
      this.showbutton = !this.showbutton
    },
    handlerClick1() {
      console.log(1, this)
    },
    handlerClick2() {
      console.log(2, this)
    },
  },
}
</script>
复制代码

I won't go into details here, just three buttons, the toggle button is used to switch the display of the other two buttons, one prints 1, and the other prints 2

Then a simplified version of v-debounce

const debounce = (fn) => {
  let timer
  return () => {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn()
    }, 300)
  }
}

const vDebounce = {
  install(Vue) {
    Vue.directive('debounce', {
      bind(el, binding) {
        el.debounceFn = debounce(binding.value)
        el.addEventListener('click', el.debounceFn)
      },
      unbind(el) {
        el.removeEventListener('click', el.debounceFn)
      },
    })
  },
}

Vue.use(vDebounce)
复制代码

There should be nothing more simplified, get it, start the project!

button1.png

At this point, click button 1, the console output is

log1.png

No problem, then click the toggle button again

button2.png

Click button 2, the console output is

log2.png

How about my 2! ! ! Students who have seen the vue source code must be able to react quickly. This must be the patchVnode method .
The easiest way to think of is to add a key to each button , can you reuse it for me?
Yes, adding the key value can really solve this problem, but you can't ask everyone who uses your command to add a key to every element that uses this command, forgetting it once is a bug ticket, hurting people Not shallow. So, let's explore what a better solution is available!

How to handle custom instructions when vue updates vnode

vue的diff算法和patch流程,掘金上已经有很多大佬详细讲过了,这里我就不赘述了,我们单独看看updateDirectives做了什么。

// 不管新旧节点,只要一边存在自定义指令,就执行_update
if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode);
  }
复制代码

_update包含了各种可能的情况,我们单独看看本次我们问题的情况,也就是新旧节点都存在自定义指令

const dirsWithPostpatch = [];

for (key in newDirs) {
    oldDir = oldDirs[key];
    dir = newDirs[key];
    dir.oldValue = oldDir.value;
    dir.oldArg = oldDir.arg;
    callHook(dir, "update", vnode, oldVnode);
    if (dir.def && dir.def.componentUpdated) {
      dirsWithPostpatch.push(dir);
    }
  }
复制代码

为方便阅读,此处我删了旧节点不存在指令的情况。
简单说,就是vue会把旧的指令参数存在对象里面,然后触发update钩子方法,然后用一个数组存起来

if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, "postpatch", () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode);
      }
    });
  }
复制代码

然后遍历这个数组,分别触发componentUpdated钩子函数
再然后,再然后就没了(¬‿¬),感情vue就光触发了两钩子。

此时,再回头看一下我们的v-debounce,我们在bind钩子中,给元素绑定了click事件,并且已经生成了防抖的事件执行方法,vue在更新vnode时,并不会重置它,导致了我们遇到的问题。

解决方案

明白问题的起因,解决起来就很简单了,我们在componentUpdated钩子中,重新绑定事件即可。

componentUpdated(el, binding) {
        el.removeEventListener('click', el.debounceFn)
        el.debounceFn = debounce(binding.value)
        el.addEventListener('click', el.debounceFn)
      },
复制代码

控制台执行结果

log3.png

完全没问题,防抖也正常,下面放下改动后的v-debounce

const debounce = (fn) => {
  let timer
  return () => {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn()
    }, 300)
  }
}

// 自定义防抖指令
const vDebounce = {
  install(Vue) {
    Vue.directive('debounce', {
      bind(el, binding) {
        el.debounceFn = debounce(binding.value)
        el.addEventListener('click', el.debounceFn)
      },
      /* 解决代码 start */
      componentUpdated(el, binding) {
        el.removeEventListener('click', el.debounceFn)
        el.debounceFn = debounce(binding.value)
        el.addEventListener('click', el.debounceFn)
      },
      /* 解决代码 end */
      unbind(el) {
        el.removeEventListener('click', el.debounceFn)
      },
    })
  },
}

Vue.use(vDebounce)
复制代码

这只是个演示用的简化v-debounce,项目中还需要封装各种参数进来,有需要可以参考下大佬们的其他文章。 有其他更优方案,也请大佬们不吝赐教~~

Guess you like

Origin juejin.im/post/7082964045996752903