事件


概述

事件可以在页面中的操作与逻辑代码绑定关联关系,在用户和代码之间建立交互。

尝试给按钮的点击事件绑定一个函数 foo

<button onclick="foo()">点击事件</button>
<script>
  function foo() {
    alert("点击就弹框");
  }
</script>

上述代码会给这个按钮注册一个点击事件,当用户点击按钮的时候,会执行全局上的 foo 函数。

其中,onclick 表示一个点击 事件foo 被叫做这个点击事件的 事件处理器,事件与事件处理器之间存在绑定关系。

事件绑定与解绑

为一个事件绑定事件处理器有三种方式:

  • 单个绑定
  • 多个绑定
  • 元素绑定

单个绑定

元素的所有事件都有对应的元素属性,通过给对应的事件属性赋值一个函数,即可绑定对应的事件处理器,比如:

var bt0 = document.getElementsByTagName("button")[0];
bt0.onclick = function () {
  console.log("事件执行了");
};
// 解绑
// bt0.onclick = null;

如果需要解绑事件,直接让对应事件的值为 null 即可。

当一个事件没有绑定时,它的默认值是 null,元素不支持事件为 undefined 你可以使用此规则判断浏览器是否支持某个事件,比如:

var foo = document.getElementById("foo");
if (foo.onclick !== undefined) {
  console.log("浏览器支持 onclick 事件");
}

多个绑定

也可以为同一个元素的同一个事件绑定多个事件处理器,使用以下方法

  • EventTarget.addEventListener:为元素的某个事件绑定一个事件处理器
  • EventTarget.removeEventListener:解绑一个事件
  • attachEvent:绑定事件,IE9 以下使用
  • detachEvent:解绑事件,IE9 以下使用

示例:

var bt0 = document.getElementsByTagName("button")[0];

function foo() {
  console.log("事件触发了");
}

bt0.addEventListener("click", foo);
// IE 低版本按照以下方式绑定
// bt.attachEvent("onclick", foo);

// 解绑
// bt0.removeEventListener(foo);
// bt0.detachEvent(foo);

如果事件处理器是一个匿名函数表达式,并且这个函数表达式没有被保存,那么将无法解绑事件,因为无法设置解绑的函数。比如:

var bt0 = document.getElementsByTagName("button")[0];

// 函数声明
function foo() {
  console.log("foo,这个事件可以解绑");
}
bt0.addEventListener("click", foo);
// 解绑
bt0.removeEventListener(foo);

// 保存函数表达式
var bar = function () {
  console.log("bar,这个事件可以解绑");
};
bt0.addEventListener("click", bar);
// 解绑
bt0.removeEventListener(bar);

// 绑定一个具名函数表达式
bt0.addEventListener("click", function baz() {
  console.log("baz,这个事件可以在处理器中解绑,单击一次后解绑");
  // 解绑
  bt0.removeEventListener(baz);
});

// 绑定一个匿名函数表达式,无法解绑
bt0.addEventListener("click", function () {
  console.log("匿名,这个事件无法解绑");
});
备注:当多个一个事件绑定多个事件处理器时,触发事件的时候它们会按照绑定顺序执行。

因为低版本 IE 没有 addEventListener,你可以是自行编写兼容代码,如:

元素绑定

可以配合 HTML 代码绑定事件:

<button onclick="foo()">click</button>
<script>
  function foo() {
    console.log("事件绑定");
  }
</script>

上述绑定事件的方式不被推荐,因为这样 JavaScript 的代码干扰了 HTML 结构。

事件对象 & 事件源

当事件触发的时候,系统会传给事件处理器一个参数,这个参数称作事件对象,里面包含了事件执行时的详细信息。 比如:

var bt0 = document.getElementsByTagName("button")[0];

bt0.addEventListener("click", function (e) {
  console.log(e);
});

事件对象中有执行事件时的各种信息,不同的事件得到的事件对象有些许不同,稍后介绍各类事件时会逐一介绍对应的事件对象。

事件执行时,并不是所有情况下都会传入事件对象,比如:

<button onclick="test()">bt</button>
<script>
  function test(e) {
    console.log(e); // undefined
  }
</script>

上面的情况可以通过主动传入事件对象来解决,比如:

<button onclick="test(event)">bt</button>
<script>
  function test(e) {
    console.log(e);
  }
</script>

IE 低版本兼容性极差,可以使用 window.event 来获取,这是不标准的写法,因为 window.event 是动态的,并不安全:

var bt0 = document.getElementsByTagName("button")[0];

bt0.onclick = function (e) {
  var event = e || window.event;
  console.log(event);
};

事件对象上的 target 属性指向触发该事件的元素,这个元素被称作 事件源。低版本的 IE 中,事件源需要由事件对象上的 srcElement 属性获取。

<button onclick="test(event)">bt</button>
<script>
  function test(e) {
    var event = e || window.event;
    var targetEl = event.target || event.srcElement;
    targetEl.style.background = "red";
  }
</script>

标准的事件源是通过 target 获取,srcElement 是兼容 IE 的写法。

事件流与冒泡捕获

事件触发时,事件会被分发到触发事件元素的父级及祖先上,观察下列代码:

<div>
  <input type="text" />
</div>
<script>
  var div = document.getElementsByTagName("div")[0];

  div.addEventListener("input", function (e) {
    console.log(e.target.value);
  });
</script>

我们在 div 元素上绑定了一个表单输入事件,input 元素的输入事件,传递给了父级,父级 div 元素监听到了子级传递过来的事件,我们把这种事件传播的行为,称作 事件流

事件流是双向的,从父到子传递的过程被称作 事件捕获、从子到父传递的过程被称作 事件冒泡。每次事件触发时,会 先进行事件捕获,再进行事件冒泡

w3c 中,有对事件流传递过程的可视化模型。

通过指定 addEventListener 的第三个参数,可以指定事件在什么阶段触发。第三个参数是一个布尔值,true 为捕获阶段触发,false 为冒泡阶段触发。(IE9 以下事件流只有冒泡阶段)

示例(查看控制台):

事件流的传播可以阻止,在事件对象上提供了停止事件传播的方法:

  • Event.stopPropagation:标准,停止事件传播。
  • cancelBubble:,停止事件冒泡,IE9 以下的事件流没有捕获阶段,只有冒泡阶段,所以 attachEvent 绑定事件就没有第三个参数。

通过以下代码,可以在上面的例子中阻止事件传播:

document.getElementById("foo2").addEventListener(
  "click",
  function (e) {
    e.stopPropagation();
  },
  true
);

如果你想让你的页面能顺畅运行在 IE9 以下,你可以在元素的原型上添加一个自定义绑定事件的方法:

function bindEvent(el, name, handle, useCatch) {
  if (el.addEventListener) {
    el.addEventListener(name, handle, useCatch);
  } else {
    el.attachEvent("on" + name, handle);
  }
}
var foo = document.getElementById("foo");
bindEvent(foo, "click", function () {
  console.log("事件绑定");
});

默认事件

很多事件有默认行为,比如超链接元素的 click 事件或者表单提交的 submit 事件,它们都有可能会跳转页面。

观察以下代码:

<form action="https://www.baidu.com/s">
  <a href="https://www.iqiyi.com/">iqiyi</a>
  <input type="text" name="wd" />
  <input type="submit" value="search" />
</form>

上述代码中,无论点击 iqiyi 按钮还是点击 search 按钮,都会发生页面跳转,这是超链接点击事件和表单提交事件的 默认行为

默认行为是可以阻止的,使用事件对象的 preventDefault 方法:

document.getElementsByTagName("form")[0].addEventListener("submit", function (e) {
  e.preventDefault();
});

document.getElementsByTagName("a")[0].addEventListener("click", function (e) {
  e.preventDefault();
});

低版本 IE 中不支持该方法,在低版本 IE 中,可以使用 event.returnValue = false 值来阻止默认事件。

document.getElementsByTagName("form")[0].addEventListener("submit", function (e) {
  e = e || window.event;
  e.returnValue = false;
  e.preventDefault && e.preventDefault();
});

各类事件

JS 中事件繁多:

  • 鼠标事件(鼠标点击,滑动)
  • 键盘事件(键盘输入)
  • 表单事件(表单改变,焦点获取或丢失等)
  • 其他

鼠标事件

事件名描述
onclick单击
ondbclick双击(如果绑定了单击事件,那么单击事件也会触发)
onmousedown鼠标按下
onmouseup鼠标松开
onmousemove鼠标移动
onmouseenter鼠标移入到该元素内部 不会冒泡
onmouseleave鼠标移出到该元素内部 不会冒泡
onmouseover鼠标移入到该元素上(从内部子级移入也会触发)
onmouseout鼠标移出到该元素上(移出到内部子级也会触发)

需要注意的是, onclickondbclick 都只有鼠标左键才能触发。

鼠标事件的事件对象都是由 MouseEvent 创建的,以下是 MouseEvent 事件对象提供的常用的属性:

属性描述
MouseEvent.offsetX/Y相对目标节点的内容边界的偏移量(在 border 上可能为负值)
MouseEvent.clientX/Y相对视区的偏移量
MouseEvent.screenX/Y相对屏幕的偏移量
MouseEvent.shiftKey是否按下辅助键 shift
MouseEvent.ctrlKey是否按下辅助键 ctrl
MouseEvent.altKey是否按下辅助键 alt

鼠标滚轮事件

事件描述
onwheel使用滚轮时触发,IE 不支持
onmousewheel类似 onwheel ,非标准事件,不建议使用,Firefox 不支持

鼠标滚轮事件由 WheelEvent 创建,它的原型是 MouseEvent,所以也可以使用 MouseEvent 的属性,WheelEvent 常用属性如下:

属性对应事件描述兼容
wheelDeltaonmousewheel滚动的抽象值Firefox 无效
wheelDeltaXonmousewheelX 轴滚动的抽象值Firefox 无效 IE 无效
wheelDeltaYonmousewheelY 轴滚动的抽象值Firefox 无效 IE 无效
deltaXonwheelX 轴滚动量IE 无效
deltaYonwheelY 轴滚动量IE 无效
deltaZonwheelZ 轴滚动量IE 无效
deltaModeonwheel滚动量的单位,0 为像素,1 为行,2 为页IE 无效

键盘事件

事件名描述
onkeydown键盘按键按下
onkeypress键盘按键按下并输入字符后触发,此事件在新标准中已废弃
onkeyup键盘按键松开

元素获取焦点后按下键盘按键会触发键盘事件,默认情况下,当点击页面中时,body 元素是获取到焦点的。

观察以下代码:

function bindKeyEvent(el, eventName, mes) {
  el.addEventListener(eventName, function (e) {
    console.log(mes);
  });
}

bindKeyEvent(document, "keydown", "down");
bindKeyEvent(document, "keypress", "press");
bindKeyEvent(document, "keyup", "up");

点击键盘按键,上述三个事件会依次执行,但若长按键盘按键, keydownkeypress 会交替执行,某些浏览器上,长按键盘按键 keyup 也会同前两个事件交替执行。

键盘事件对象由 KeyboardEvent 创建,KeyboardEvent 会提供一下常用属性:

属性描述
MouseEvent.shiftKey是否按下辅助键 shift
MouseEvent.ctrlKey是否按下辅助键 ctrl
MouseEvent.altKey是否按下辅助键 alt
code键码
key键值

有一些其他的键码或键值,比如 keyCodecharcharCodewhich 等,它们可能是非标准的或者在新标准中被废弃,不建议使用。

表单事件

form 元素

事件名描述补充
onsubmit提交表单当表单内,属性 type="submit"input 元素或者 button 元素被点击的时触发
onreset重置表单当表单内,属性 type="reset"input 元素被点击时触发

input、select、textarea 元素

事件名描述
onchange焦点转移后有数据改变时触发
oninput数据改变时触发,低版本 IE 不兼容

其他事件

  • 焦点

    • focus 不冒泡
    • blur 不冒泡
  • 滚动事件滚轮

    • scroll 不冒泡
  • 窗口大小变化

    • resize 不冒泡

剩余事件查表

补充

主动触发事件

每个事件都可以主动触发,在元素属性上,有各类事件的触发方法,比如 onclick 事件使用 click 方法触发:

<button id="foo">click</button>
<script>
  var foo = document.getElementById("foo");
  foo.addEventListener("click", function () {
    var pEl = document.createElement("p");
    pEl.innerText = "content ";
    document.body.appendChild(pEl);
  });

  // 就算不主动点击按钮,每过 1.5 秒也会触发 foo 元素的点击事件
  setInterval(function () {
    foo.click();
  }, 1500);
</script>

所有事件都能主动触发,元素上有各个事件的执行函数。

javascript 协议

我们的代码可以以内联的方式写在 HTML 元素中,所有的事件都支持 javascript 协议。比如:

<button onclick="javascript:alert('what?')">click</button>

在 HTML 的事件属性中,javascript: 这个前缀可以省略,于是变成:

<button onclick="alert('what?')">click</button>

你甚至可以编写多行代码甚至复杂的语句:

<button
  id="foo"
  onclick="
    for(var i = 0 ; i < 100;i++){
      console.log(i);
    }
  "
>
  click
</button>

现在再来看看 HTML 内联事件绑定的方式:

<button id="foo" onclick="clickHandle()">click</button>
<script>
  function clickHandle() {
    // ...
  }
</script>

是不是一目了然了,它其实不是特殊语法,而是点击按钮的时候,执行了一个函数。

javascript 协议不止可以编写到事件上,还可以通过 href 属性放在 a 元素中,在点击链接的时候从而执行 JavaScript 代码,比如:

<a href="javascript: alert(1)">click</a>

如果最后一个语句的值不为 undefined 那么页面会展示链接里的内容:

<a href="javascript:'<strong>这里是链接里的内容</strong>'">click</a>

点击后就出大问题了,页面的没有跳转,但是展示了新的内容,并且不能通过后退键返回上级,只能刷新页面(IE 低版本刷新都没用,需要新建标签页)。

为了防止上述情况发生,在很久很久很久以前,让一个链接无法跳转可以编写以下代码:

<a href="javascript: void(0);">click</a>

配合 void 关键字,表达式的值永远是 undefined,可以让链接不进行跳转并且不渲染其他内容,你可以在圆括号内编写任何代码而不影响页面。但是这种方式现在已经不再提倡,因为超链接的语义就是指定一个网络资源的地址,如果为了防止跳转而设置无用的地址,完全是本末倒置。 这里介绍的原因是很多老旧页面还存在这样的链接,相当于知识点补漏。

如果需要让一个链接无法跳转,应该使用 JS 禁止其默认事件。比如:

<a href="https://www.bilibili.com" data-prevent>bilibili</a>
<script>
  document.body.addEventListener(
    "click",
    function (e) {
      e = e || window.event;
      var target = e.target || e.srcElement;
      if (target.hasAttribute("data-prevent")) {
        e.returnValue = false;
        e.preventDefault && e.preventDefault();
      }
    },
    false
  );
</script>

上面代码中,设置自定义布尔属性 data-prevent 的元素都会被禁止默认事件,从而让 a 元素无法产生跳转。

备注:非常不提倡把 JavaScript 内联写到 HTML 中。