为什么 $("a:focus").length 的计算结果为 0,除非包含在 setTimeout() 中?

Why does $("a:focus").length evaluate to 0 unless wrapped in setTimeout()?

我正在将键盘导航添加到我网站的主菜单中,并且我希望在用户在主菜单之外使用 Tab 键时关闭所有打开的子菜单。所以我有以下功能:

var $nav = $(".navigation nav.main"),
    $navItems = $nav.find("a");

$navItems.on("blur", function() {
    if ($nav.find("a:focus").length === 0) {
        closeMenu();
    }
});

我希望它的工作方式是这样的:

  1. 每当菜单中的任何 link 失去焦点时...
  2. 如果菜单中没有 link有焦点,关闭菜单

但实际发生的是 $(nav.find("a:focus").length) 总是 求值为 0,即使我可以直接在屏幕上看到确实有一个 link 焦点位于菜单内。

但是,如果我将 setTimeout() 包裹在条件语句周围,那么它会按照我的预期工作:

var $nav = $(".navigation nav.main"),
    $navItems = $nav.find("a");

$navItems.on("blur", function() {
    setTimeout(function() {
        if ($nav.find("a:focus").length === 0) {
            closeMenu();
        }
    });
});

现在,每次我点击 Tab 时,$nav.find("a:focus").length 的计算结果为 1,直到我在菜单外按 Tab 键,此时它的计算结果为 0 并关闭它。

这正是我想要的工作方式,但为什么 setTimeout() 是必需的?

Why doesn't it work?

正如 epascarello 在评论中指出的那样,您的 blur 发生在下一个元素获得焦点之前。

Why does setTimeout(), even with a 0ms delay, work?

相信这与渲染引擎有关。当您执行 setTimeout() 时,该函数会 "queued" 落后于 渲染引擎的 UI 更新(关注下一个元素)。尽管 time 没有区别,setTimeout() 确保检查发生 after UI update.

事件顺序:

1. Fire blur event
2. Update UI

具有 setTimeout() 的事件顺序:

1. Fire blur event (queue new function) ──┐
2. Update UI                              |
3. Run the queued function    <───────────┘

Is there an alternative?

blur 事件的 relatedTarget 表示哪个元素将获得焦点。

通过了解即将获得焦点的元素,您可以使用 jQuery 的 .filter() 来确定它是否是导航项。

var $nav = $(".navigation nav.main"),
    $navItems = $nav.find("a");

$navItems.on("blur", function(e) {
  var $focusedElement = $(e.relatedTarget);
  var isNavItem = !!$navItems.filter($focusedElement).length;
  if (!isNavItem)
    console.log("The focused element is not a nav item.");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="navigation">
  <nav class="main">
    <a href="#">Link 1</a>
    <a href="#">Link 2</a>
  </nav>
</div>

<a href="#">Non-nav link</a>