浏览器默认动作

许多事件会自动触发浏览器动作。

例如:

  • 单击一个链接 —— 触发到它的 URL。
  • 单击表单中的提交按钮 —— 触发提交到服务器的动作。
  • 在文本上按下鼠标按键并移动 —— 选中文本。

如果我们用 JavaScript 处理一个事件,我们通常不需要浏览器动作。幸运的是,它是可以阻止的。

阻止浏览器动作

有两种方法可以告诉浏览器我们不希望它执行动作:

  • 主流的方法是使用 event 对象。有一个 event.preventDefault() 方法。
  • 如果使用 on<event>(而不是 addEventListener)分发处理器,那么我们只需要从它内部返回 false 即可。

在下面的示例中,单击链接不会导致 URL 改变:

<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="event.preventDefault()">here</a>
没有必要去返回 true

事件处理器返回的值通常会被忽略。

唯一的例外 —— 是从使用 on<event> 分发的处理器中 return false

在所有其他情况下,返回都是不需要的,也不需要被处理。

示例:菜单

考虑一个站点菜单,如下所示:

<ul id="menu" class="menu">
  <li><a href="/html">HTML</a></li>
  <li><a href="/javascript">JavaScript</a></li>
  <li><a href="/css">CSS</a></li>
</ul>

下面是一些 CSS 的外观:

菜单项是 <a> 链接,而不是按钮。这有几个好处,比如:

  • 许多人喜欢使用“右键” —— “打开一个新窗口”。如果我们使用 <button> 或者 <span>,这些动作都会失效。
  • 搜索引擎在索引时遵循 <a href="...">

因为我们在标记中使用 <a>。但通常我们打算用 JavaScript 处理单击。因此我们应该阻止浏览器默认动作。

就像这样:

menu.onclick = function(event) {
  if (event.target.nodeName != 'A') return;

  let href = event.target.getAttribute('href');
  alert( href ); // ...可以从服务器和 UI 等加载

  return false; // prevent browser action (don't go to the URL)
};

如果我们省略 return false,那么在我们的代码执行后,浏览器将执行它的“默认动作” —— 在 href 中跟踪 URL。

顺便说一句,这里使用事件委托会使我们的菜单更灵活。我们可以添加嵌套列表并使用 CSS 对其样式设置 “slide down”。

阻止进一步的事件

某些事件流入另一个事件。如果我们阻止第一个事件,就没有第二个事件。

例如,在 <input> 上的 mousedown 会导致在其中获得焦点,也就是 focus 事件。如果我们阻止 mousedown 事件,就不会有焦点。

尝试点击以下的 <input> —— focus 事件会发生。这很正常。

但是如果你点击第二个,就会失去焦点。

<input value="Focus works" onfocus="this.value=''">
<input onmousedown="return false" onfocus="this.value=''" value="Click me">

这是因为浏览器动作在 mousedown 上被取消。如果我们用另一种方式进行输入,焦点仍然有用。例如,Tab 键用于从第一个输入切换到第二个输入。但不要再用鼠标单击了。

event.defaultPrevented

如果默认动作被阻止,那么 event.defaultPrevented 属性为 true,否则为 false

还有一个有趣的用例。

你还记得在 冒泡和捕获 章节中,我们讨论过 event.stopPropagation() 以及为什么停止冒泡是不好的么?

有时我们可以使用 event.defaultPrevented 来代替。

我们来看一个实际的例子,停止冒泡看起来是必须的,但实际上没有它我们也可以做的很好。

默认情况下,contextmenu 事件(鼠标右击)上的浏览器显示一个带有标准选项的上下文菜单。我们可以阻止它并显示我们自定义的菜单,就像这样:

<button>Right-click for browser context menu</button>

<button oncontextmenu="alert('Draw our menu'); return false">
  Right-click for our context menu
</button>

现在,假设我们用我们自己的选项实现我们自己文档范围的上下文菜单。在文档中,我们可能有其他元素和它们自己的上下文菜单:

<p>Right-click here for the document context menu</p>
<button id="elem">Right-click here for the button context menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

问题是当我们点击 elem 时,我们得到两个菜单:按钮级别(事件冒泡)和文档级别的菜单。

如何修复呢?其中一个解决方案是:“在按钮处理器中,我们全部去处理(按钮级别的)事件,然后停止它。”还要使用 event.stopPropagation()

<p>Right-click for the document menu</p>
<button id="elem">Right-click for the button menu (fixed with event.stopPropagation)</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    event.stopPropagation();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Document context menu");
  };
</script>

现在按钮级别的菜单如期工作。但是代价太大,我们会永远拒绝访问任何外部代码的右击信息,包括收集统计信息的计数器等等。这很不可取。

另一个替代方案是,在文档级处理器中去检测默认动作是否被阻止?如果是这样的话,那么事件就被处理了,我们不需要对它做出反应。

<p>Right-click for the document menu (fixed with event.defaultPrevented)</p>
<button id="elem">Right-click for the button menu</button>

<script>
  elem.oncontextmenu = function(event) {
    event.preventDefault();
    alert("Button context menu");
  };

  document.oncontextmenu = function(event) {
    if (event.defaultPrevented) return;

    event.preventDefault();
    alert("Document context menu");
  };
</script>

现在一切都可以正常工作了。如果我们有嵌套元素,并且每个元素都有自己的上下文菜单,那么这也是可以运行的。只需确保检查每个 contextmenu 处理器中的 event.defaultPrevented

event.stopPropagation() 和 event.preventDefault()

正如我们所看到的那样,event.stopPropagation()event.preventDefault()(也被认为是 return false)是两种不同的事情。它们之间毫无联系。

嵌套的上下文目录结构

还有一些实现嵌套上下文菜单的替代方法。其中一个是拥有一个特殊的全局对象,它具有处理 document.oncontextmenu 的方法,还允许在其中存储各种“低级”处理器方法。

对象将捕获任何右击事件,查看存储的处理器并运行相应的处理器。

但每一段需要上下文菜单的代码都应该了解该对象,并使用它的帮助,而不是使用自己的 contextmenu 处理器。

总结

有许多默认浏览器动作:

  • mousedown —— 开始选择(移动鼠标进行选择)。
  • <input type="checkbox">click —— 检查/取消选中的 input
  • submit —— 单击 <input type="submit"> 或在表单中通过单击 Enter 触发该事件,并在其后浏览器提交表单。
  • wheel —— 鼠标滚轮事件的滚动将作为默认动作。
  • keydown —— 按下按键可能会导致将字符添加到字段,或者触发其他动作。
  • contextmenu —— 事件发生在右击时,动作是显示浏览器上下文菜单。
  • …还有更多…

如果我们想要通过 JavaScript 来处理事件,那么所有的默认动作都可以被阻止。

想要阻止默认行为 —— 可以使用 event.preventDefault() 或者 return false。第二个方法只适用于分发了 on<event> 的处理器。

如果默认动作被阻止,event.defaultPrevented 的值就会变成 true,否则会变成 false

Stay semantic, don’t abuse

从技术上来说,通过阻止默认动作和添加 JavaScript,我们可以定制任何元素的行为。例如,我们可以使链接 <a> 像按钮一样工作,而按钮 <button> 也可以作为链接运行(重定向到另一个 URL 等)。

但我们通常应该保留 HTML 元素的语义。例如 <a> 应该表现为导航,而不是按钮。

除了“是一件好事”,这使你的 HTML 在可访问性方便表现得更好。

另外,如果我们考虑 <a> 的示例,那么请注意:浏览器允许在一个新窗口中打开这样的链接(右击它们以及其他方法)。大家都喜欢这么做。但如果我们使用 JavaScript 让按钮行为表现得像链接,甚至看起来像 CSS 的链接,那么 <a> 独特的浏览器特性将仍然不会适用于按钮。

任务

重要程度: 3

为什么下述代码 return false 不起作用?

<script>
  function handler() {
    alert( "..." );
    return false;
  }
</script>

<a href="http://w3.org" onclick="handler()">the browser will go to w3.org</a>

浏览器在单击时会根据 URL 进行跳转,但这不是我们想要的。

如何修复?

当浏览器读取如 onclick 这样的 on* 属性时,它会根据内容创建一个处理程序。

onclick="handler()" 来说函数是:

function(event) {
  handler() // the content of onclick
}

现在我们可以看到 handler() 返回值没有被使用,也没有对结果产生影响。

修复方法很简单:

<script>
  function handler() {
    alert("...");
    return false;
  }
</script>

<a href="http://w3.org" onclick="return handler()">w3.org</a>

也可以像这样使用 event.preventDefault()

<script>
  function handler(event) {
    alert("...");
    event.preventDefault();
  }
</script>

<a href="http://w3.org" onclick="handler(event)">w3.org</a>
重要程度: 5

在所有包含 id="contents" 属性元素的链接(在触发单击事件时)询问用户是否真的想离开当前页面。

就像这样:

细节:

  • 元素中的 HTML 可以随时动态加载或者重新生成,因此我们无法找到所有链接并将处理器放在它们身上。这里使用事件委托。
  • 内容可能有嵌套标签。内部链接也是这样,比如 <a href=".."><i>...</i></a>

打开一个任务沙箱。

这是一个使用事件委托模式的案例。

在现实中,我们可以向服务器发送一个 “logging” 请求而不是询问,以保存访问者离开位置的信息。或者我们可以加载内容并将其显示在页面中(如果允许的话)。

我们只需要捕捉 contents.onclick 然后使用 confirm 来询问用户。一个好主意是使用 link.getAttribute('href') 来代替 link.href。详情请参阅解决方案。

使用沙箱打开解决方案。

重要程度: 5

创建一个图像库,通过单击缩略图来更改主图像。

就像这样:

P.S. 使用事件委托。

打开一个任务沙箱。

解决方案是将处理器分发给容器并追踪点击。如果单击 <a> 上的链接,则将 #largeImgsrc 修改位缩略图的 href

使用沙箱打开解决方案。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。