当函数被聚焦和点击事件同时触发时,如何只运行一次该函数?

11

我有一个 input 元素,它绑定了两个事件:focus 和 click。它们都会触发同一个帮助函数。

当我使用 tab 键聚焦到该输入框时,focus 事件会触发,我的帮助函数也只执行一次,没有问题。

当该元素已经获得焦点,我再次单击它时,click 事件会触发,帮助函数也只会执行一次,没有问题。

但当该元素没有焦点,并且我单击它时,这两个事件都会触发,帮助函数将被执行两次。如何让这个帮助函数只执行一次呢?

我在这里看到了几个类似的问题,但是没有真正理解它们的答案。我还发现了 .live jQuery 处理程序,如果我让它监视一个状态类,似乎可以起作用。但是似乎应该有更简单的方法。.one 处理程序可以起作用,但我需要它能够多次执行。

谢谢任何帮助!

7个回答

8
这里最好的解决方案是设计一个不会在同一用户操作上触发两个不同事件的设计,但由于您没有解释您正在编写的整体问题,我们无法帮助您采用这种方法。
一种方法是防止单个事件触发相同操作的最佳方法是“防抖”函数调用,并且仅在给定元素中从未很快地调用该函数(例如可能来自同一用户事件)时才调用该函数。 您可以通过记录此元素上一次触发的时间并仅在时间长于某个值时调用该函数来实现此目的。
以下是一种可能的方法:
function debounceMyFunction() {
    var now = new Date().getTime();
    var prevTime = $(this).data("prevActionTime");
    $(this).data("prevActionTime", now);
    // only call my function if we haven't just called it (within the last second)
    if (!prevTime || now - prevTime > 1000) {
        callMyFunction();
    }
}

$(elem).focus(debounceMyFunction).click(debounceMyFunction);

谢谢回答。我也考虑过检查时间,但希望有更优雅的方法。不过还是非常感谢您提供的代码示例! - dylanized
@dylanized - 我目前不知道有什么办法可以在不考虑时间的情况下知道一个给定的点击事件是否也触发了焦点。 - jfriend00
@jfriend00 - focus 总是被首先调用。focus 总是被首先调用的原因在于它将在mousedown事件发生时触发,然而click事件需要mousedown和mouseup两个事件一起发生。 - Travis J
@TravisJ - 但是知道焦点总是首先被调用并不能解决问题,因为在OP的情况下,有时根本不会调用焦点(如果元素已经具有焦点)。 OP需要知道当他们点击时是否刚生成了焦点事件。 我的建议是使用时间来知道这一点,因为我不知道其他任何方法来知道它。 仅仅在焦点上设置一个简单的标志无法单独使用,因为某些时间后跟随点击事件的焦点事件(例如从制表符到字段)需要处理两个事件。 - jfriend00

3
你可以使用一个被清除和设置的超时来实现。这会引入轻微的延迟,但确保只触发最后一个事件。
$(function() {
  $('#field').on('click focus', function() {
    debounce(function() {
        // Your code goes here.
        console.log('event');
    });
  });  
});

var debounceTimeout;

function debounce(callback) {
  clearTimeout(debounceTimeout);
  debounceTimeout = setTimeout(callback, 500);
}

这是一个 jsfiddle 的链接 http://jsfiddle.net/APEdu/

更新

为了回应其他评论中关于使用全局变量的问题,你可以将 doubleBounceTimeout 设为一个包含在事件处理程序中传递的键的多个超时集合。或者你可以将相同的超时传递给任何处理相同事件的方法。这样,你可以使用相同的方法来处理任意数量的输入。


3
这对我有用: http://jsfiddle.net/cjmemay/zN8Ns/1/
$('.button').on('mousedown', function(){
    $(this).data("mouseDown", true);
});

$('.button').on('mouseup', function(){
    $(this).removeData("mouseDown");
});

$('.button').on('focus', function(){
    if (!$(this).data("mouseDown"))
        $(this).trigger('click.click');
});

$(".button").on('click.click',evHandler);

我是直接从这里偷来的: https://dev59.com/f3E95IYBdhLWcg3wGqAH#9440580

不错的解决方案。我猜这个能够工作是因为我们在聚焦之前排队了mousedown事件?也就是说,如果我们在聚焦之前放置mousedown事件,它将无法工作? - dylanized
@dylanized,我认为它能起作用,是因为单击时mousedown事件会先触发,然后才是focus事件。mousedown, focus和mouseup的顺序。因此,当事件是聚焦时,它会检查元素上是否设置了mousedown标志。在这里,切换选项卡和按下空格键并不重要。也就是说,我认为事件在代码中注册的顺序并不重要,重要的是它们触发的顺序。 - Christopher Meyers
我发现的唯一注意事项是,如果你在输入框上按下鼠标,然后移开,它不会清除标志,从而破坏了焦点事件。我在这里尝试解决这个问题,但这可能与您的文档结构、js以及其用途有关:http://jsfiddle.net/cjmemay/zN8Ns/2/ - Christopher Meyers
发现了另一个这种方法存在问题的边缘情况:标签。如果您单击与输入相关联的标签,它会将焦点发送到输入,但是鼠标事件中没有任何一个被注册。因此,evHandler会执行两次。解决方案是也监视标签的mousedown/click事件,但在那一点上可能更简单的方法是采用防抖动(debounce)方法... - dylanized

1

现场演示(点击)

当元素被点击第一次(获得焦点)时,我只是简单地设置一个标志来阻止点击。然后,如果该元素通过制表键获得焦点,则也会删除该标志,以使第一次点击有效。

var $foo = $('#foo');

var flag = 0;
$foo.click(function() {
  if (flag) {
    flag = 0;
    return false;
  }
  console.log('clicked');
});

$foo.focus(function() {
  flag = 1;
  console.log('focused');
});

$(document).keyup(function(e) {
  if (e.which === 9) {
    var $focused = $('input:focus');
    if ($focused.is($foo)) {
      flag = 0;
    }
  }
});

有几种情况下这个方法不起作用。如果焦点设置方式不是通过Tab键或点击(例如自动聚焦、编程聚焦等),或者如果所有按键都不能向上传播到文档。此外,这种实现方式不太适合在多个地方使用,因为它使用了全局标志。 - jfriend00
@jfriend00,你知道全局标志不是必要的,但关于其余部分的观点很好。 - m59

1
我觉得你实际上不需要点击处理程序。听起来这个事件附加到一个元素上,当被点击时获得焦点并触发焦点处理程序。因此,无论何时点击它都会触发你的焦点处理程序,所以你只需要焦点处理程序。
如果不是这种情况,那么很遗憾,没有容易的方法来实现你所要求的。在焦点上添加/删除类,并且只有在类不存在时才触发点击,是我能想到的唯一方法。
我有两个选择:
1-将单击处理程序绑定到焦点回调中的元素
2-将焦点和单击处理程序绑定到不同的类,并使用焦点回调添加单击类,并使用模糊来删除单击类

谢谢您的回答。我需要点击处理程序,用于以下情况:当输入框已经具有焦点,但第二次/第三次/第四次被单击时。 - dylanized
@LiamBailey 这并没有任何作用,因为它在触发点击函数之前获取了焦点。这个 if 语句是无意义的。 - m59
那么这两个选项怎么样呢? - Liam Bailey

0

感谢大家的精彩讨论。看起来@jfriend00提出的去抖动解决方案和Chris Meyers提出的mousedown解决方案都是不错的处理方式。

我再想了想,也想到了这个解决方案:

// add focus event
$myInput.focus(function() {
    myHelper();
    // while focus is active, add click event
    setTimeout(function() {
        $myInput.click(function() {
            myHelper(); 
        });
    }, 500);    // slight delay seems to be required
});

// when we lose focus, unbind click event
$myInput.blur(function() {
    $myInput.off('click');
});

但似乎其他的那些稍微更加优雅。我特别喜欢Chris的,因为它不涉及时间处理。

再次感谢!


如果你无论如何都要使用超时,为什么不使用我的方法呢?它不需要绑定和解绑。 - crad
我同意你的方法更简洁。只是加上这个因为它似乎是一个可行的选项。我想我会尝试mousedown的方法,但如果那行不通,我会使用你的方法! - dylanized

0

改进@Christopher Meyers的解决方案。

简介:在单击事件触发之前,有2个事件先行,即mousedown和mouseup。如果触发了mousedown,则我们知道可能会触发mouseup。 因此,我们可能不希望焦点事件处理程序执行其操作。如果用户开始单击按钮然后将光标拖动到其他地方,则mouseup不会触发。为此,我们使用blur事件。

  let mousedown = false;

  const onMousedown = () => {
    mousedown = true;
  };

  const onMouseup = () => {
    mousedown = false;
    // perform action
  };

  const onFocus = () => {
    if (mousedown) return;
    // perform action
  };

  const onBlur = () => {
    mousedown = false;
    // perform action if wanted
  };

将会附加以下事件:

const events = [
    { type: 'focus', handler: onFocus },
    { type: 'blur', handler: onBlur },
    { type: 'mousedown', handler: onMousedown },
    { type: 'mouseup', handler: onMouseup }
];

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