当绑定到输入框的焦点和点击事件上时,防止函数触发两次。

8
我尝试让一个事件在点击聚焦时都触发,但我只想让它触发一次。当我在输入框内点击时,它会触发两次(点击和聚焦)。如何防止这种情况发生?
$('input').on('focus click', function(){
    console.log('fired');
});

顺便说一句,我已经用一个通用的去抖动器更新了我的原始答案。 - T.J. Crowder
3个回答

10

您可以使用.one代替。这将仅允许事件触发一次,但在触发一次后也会删除绑定:

$('input').one('focus click', function(){
    console.log('fired');
});

如果你需要保持绑定,你就必须跟踪鼠标按钮的状态和触发 mousedown 事件的当前目标:

var mouseDown, currentTarget;
$('input').on({
    "mousedown mouseup": function (e) {
        mouseDown = e.type === "mousedown";
        currentTarget = e.target;
    },
    "focus click": function (e) {
        if (mouseDown && currentTarget === e.target) return;
        console.log('fired');
    }
});

请查看在 jsFiddle 上的测试用例:http://jsfiddle.net/mekwall/rBzqg/1/


1
但这会移除绑定...这不是我想要的。 - kylex
@kylex 添加了 stopImmediatePropagation 方法,应该可以解决问题...我认为这是一个 bug,它不应该出现。我会继续研究一下,因为我有点不喜欢 T.J 的解决方案,尽管它能够工作。 - mekwall
2
@MarcusEkwall:stopImmediatePropagation 停止 该事件(例如 click)传播到其他处理程序。它不会阻止浏览器因当前事件而产生的其他事件(例如 focus)。 - T.J. Crowder
1
刚刚意识到上一个方法还有另一个问题。如果连续触发两个“focus”事件,它将无法正确工作,因为它不会注册第二个“focus”。@kylex,我可以问一下为什么您需要绑定点击事件,因为点击将触发焦点,这应该是不必要的吧? - mekwall
@MarcusEkwall,我绑定了两个标签,因为它不仅仅是输入触发它。它也可以是div或span。我只展示了input,因为那是引起问题的标签。 - kylex
@kylex 好的!我修改了我的答案。现在它包括一个完全可用的解决方案。 - mekwall

8

可以考虑加入一些滞后效应。基本上记录您上次响应任何事件的时间,并在守卫时间内忽略后续事件。

您可以使用jQuery的data(答案末尾的示例),但我更喜欢这个:一个通用的防抖器:

使用clickfocus的实时示例 | 实时源代码

$("#field").on("click focus", debounce(100, function(e) {
  // Event occurred, but not with 100ms of the previous one
}));

debouncer函数:

// debounce - debounces a function call
//
// Usage: var f = debounce([guardTime, ] func);
//
// Where `guardTime` is the interval during which to suppress
// repeated calls, and `func` in the function to call.
// You use the returned function instead of `func` to get
// debouncing;
//
// Example: Debouncing a jQuery `click` event so if it happens
// more than once within a second (1,000ms), subsequent ones
// are ignored:
//
//    $("selector").on("click", debounce(1000, function(e) {
//      // Click occurred, but not within 1000ms of previous
//    });
//
// Both `this` and arguments are passed through.
function debounce(guardTime, func) {
  var last = 0;

  if (typeof guardTime === "function") {
    func = guardTime;
    guardTime = 100;
  }
  if (!guardTime) {
    throw "No function given to debounce";
  }
  if (!func) {
    throw "No func given to debounce";
  }

  return function() {
    var now = +new Date();
    if (!last || (now - last) > guardTime) {
      last = now;
      return func.apply(this, arguments);
    }
  };
}

(“去抖动器”是一个常用术语,用于使用滞后特性限制输入。如果我没记错,它来自“开关去抖动器”,这是一种(非常)简单的电路,用于避免在机械触发式电开关从打开到关闭和反之转换时多次触发动作,因为当接点靠近时,可以出现大量的闭合/开启/闭合/开启/闭合/开启抖动,直到开关达到稳态。这种抖动被称为“bouncing”,因此,“去抖动器”。)

仅使用jQuery的data方法的方法:

$('input').on('focus click', function(){
    var $this = $(this);
    var now = +new Date();
    var lastClicked = $this.data("lastClicked");
    if (lastClicked && (now - lastClicked) < 100) {
        // Don't do anything
        return;
    }
    $this.data("lastClicked", now);

    // Do the work
});

+1 有点巧妙,但是是一个可靠的解决方法。(我仍然会注意边缘情况) - winner_joiner
使用滞后器或去抖动器的问题在于,如果您单击“输入”并按住按钮超过100毫秒,则会同时触发“单击”和“焦点”。 - mekwall
Underscore.js有一个防抖的实现,详见这里 - P. Avery

1

这是一个老问题,但我找不到其他解决我的问题的答案。因此,我在这里发布它,供那些在2015年遇到此问题的人参考。

$('#menu-button').on('click focus', function() {
  if(!$(this).is(':focus')) { // 1
    // Do stuff once
  }
  else {
    $this.blur(); // 2
  }
});
  1. 这仅在单击时触发事件。我不确定背后发生了什么,也许有人可以解释一下,但选项卡和焦点似乎没有受到影响,完全正常。

  2. 这取消了所选对象的焦点,但将焦点路径设置回文档顶部。我保留此处,以便我可以再次单击所选元素以禁用菜单。我仍在寻找保持焦点路径的解决方法。

编辑:更好的方法:

$('#menu-button').on('click focus', function(event) {
  if(event.type === 'focus') { // 1
    // Do stuff once
  }
});
  1. 点击会触发焦点,但是焦点不会触发点击。所以只需在焦点事件上运行代码。

谢谢您的帖子。我喜欢您的“更好的方法”。但是我正在使用1.7.2版本的jQuery,它给了我“focusin”而不是“focus”。感谢您提供一行解决方案。 - whitesiroi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接