我想你极可能据说过事件驱动
, 可是事件驱动究竟是什么?为何说浏览器是事件驱动的呢?为何 NodeJS 也是事件驱动的 ? 二者是一回事么?html
实际上不论是浏览器仍是 Nodejs 都是事件驱动的,都有本身的事件模型。在这里,咱们只讲解浏览器端的事件模型,若是对 Nodejs 事件模型感兴趣的,请期待个人 Nodejs 部分的讲解。node
事件驱动通俗地来讲就是什么都抽象为事件
。react
浏览器依靠事件来驱动APP运行下去,若是没有了事件驱动,那么APP会直接从头至尾运行完,而后结束,事件驱动是浏览器的基石。面试
本篇文章不讲解事件循环的内容,事件循环部分会在本章的其余章节讲解,敬请期待。浏览器
其实现实中的红绿灯就是一种事件,它告诉咱们如今是红灯状态,绿灯状态,仍是黄灯状态。 咱们须要根据这个事件本身去完成一些操做,好比红灯和黄灯咱们须要等待,绿灯咱们能够过马路。网络
下面咱们来看一个最简单的浏览器端的事件:并发
html代码:框架
<button>Change color</button>
复制代码
js代码:函数
var btn = document.querySelector('button');
btn.onclick = function() {
console.log('button clicked')
}
复制代码
代码很简单,咱们在button上注册了一个事件,这个事件的handler是一个咱们定义的匿名函数。当用户点击了这个被注册了事件的button的时候,这个咱们定义好的匿名函数就会被执行。性能
咱们有三种方法能够绑定事件,分别是行内绑定
,直接赋值
,用addEventListener
。
这个方法很是不推荐
html代码:
<button onclick="handleClick()">Press me</button>
复制代码
而后在script标签内写:
function handleClick() {
console.log('button clicked')
}
复制代码
和我上面举的例子同样:
var btn = document.querySelector('button');
btn.onclick = function() {
console.log('button clicked')
}
复制代码
这种方法有两个缺点
btn.onclick = functionA;
btn.onclick = functionB;
复制代码
这样只有functionB有效,这能够经过addEventListener
来解决。
addEventListener
来解决。所以addEventListener横空出世,这个也是目前推荐的写法。
旧版本的addEventListener
第三个参数是bool,新版版的第三个参数是对象,这样方便以后的扩展,承载更多的功能, 咱们来重点介绍一下它。
addEventListener能够给Element,Document,Window,甚至XMLHttpRequest等绑定事件,当指定的事件发生的时候,绑定的回调函数就会被以某种机制进行执行,这种机制咱们稍后就会讲到。
语法:
target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
target.addEventListener(type, listener[, useCapture, wantsUntrusted ]); // Gecko/Mozilla only
复制代码
type是你想要绑定的事件类型,常见的有click, scroll, touch, mouseover等,旧版本的第三个参数是bool,表示是不是捕获阶段,默认是false,即默认为冒泡阶段。新版本是一个对象,其中有capture(和上面功能同样),passive和once。 once用来执行是否只执行一次,passive若是被指定为true表示永远不会执行preventDefault()
,这在实现丝滑柔顺的滚动的效果中很重要。更多请参考Improving scrolling performance with passive listeners
实际上,咱们如今大多数状况都是用框架来写代码,所以上面的状况其实在现实中是很是少见的,咱们更多看到的是框架封装好的事件,好比React的合成事件,感兴趣的能够看下这几篇文章。
虽然咱们不多时候会接触到原生的事件,可是了解一下事件对象,事件机制,事件代理等仍是颇有必要的,由于框架的事件系统至少在这方面仍是一致的,这些内容咱们接下来就会讲到。
全部的事件处理函数在被浏览器执行的时候都会带上一个事件对象,举个例子:
function handleClick(e) {
console.log(e);
}
btn.addEventListener('click', handleClick);
复制代码
这个e就是事件对象,即event object
。 这个对象有一些颇有用的属性和方法,下面举几个经常使用的属性和方法。
属性
...
方法
...
前面讲到了事件默认是绑定到冒泡阶段的,若是你显式令useCapture为true,则会绑定到捕获阶段。
事件捕获颇有意思,以致于我会常常出事件的题目加上一点事件传播的机制,让候选人进行回答,这很能体现一我的的水平。了解事件的传播机制,对于一些特定问题有着很是大的做用。
一个Element上绑定的事件触发了,那么其实会通过三个阶段。
从最外层即HTML标签开始,检查当前元素有没有绑定对应捕获阶段事件,若是有则执行,没有则继续往里面传播,这个过程递归执行直到触达触发这个事件的元素
为止。
伪代码:
function capture(e, currentElement) {
if (currentElement.listners[e.type] !== void 0) {
currentElement.listners[e.type].forEach(fn => fn(e))
}
// pass down
if (currentElement !== e.target) {
// getActiveChild用于获取当前事件传播链路上的子节点
capture(e, getActiveChild(currentElement, e))
} else {
bubble(e, currentElement)
}
}
// 这个Event对象由引擎建立
capture(new Event(), document.querySelector('html'))
复制代码
上面已经提到了,这里省略了。
从触发这个事件的元素
开始,检查当前元素有没有绑定对应冒泡阶段事件,若是有则执行,没有则继续往里面传播,这个过程递归执行直到触达HTML为止。
伪代码:
function bubble(e, currentElement) {
if (currentElement.listners[e.type] !== void 0) {
currentElement.listners[e.type].forEach(fn => fn(e))
}
// returning
if (currentElement !== document.querySelector('html')) {
bubble(e, currentElement.parent)
}
}
复制代码
上述的过程用图来表示为:
若是你不但愿事件继续冒泡,能够用以前我提到的stopPropagation
。
伪代码:
function bubble(e, currentElement) {
let stopped = false;
function cb() {
stopped = true;
}
if (currentElement.listners[e.type] !== void 0) {
currentElement.listners[e.type].forEach(fn => {
fn({
...e,
stopPropagation: cb
});
if (stopped) return;
})
}
// returning
if (currentElement !== document.querySelector('html')) {
bubble(e, currentElement.parent)
}
}
复制代码
利用上面提到的事件冒泡机制,咱们能够选择作一些有趣的东西。 举个例子:
咱们有一个以下的列表,咱们想在点击对应列表项的时候,输出是点击了哪一个元素。
HTML代码:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
复制代码
JS代码:
document.querySelector('ul').addEventListener('click', e => console.log(e.target.innerHTML))
复制代码
上面说了addEventListener
会默认绑定到冒泡阶段,所以事件会从目标阶段开始,向外层冒泡,到咱们绑定了事件的ul上,ul中经过事件对象的target属性就能获取到是哪个元素触发的。
“事件会从目标阶段开始”,并非说事件没有捕获阶段,而是咱们没有绑定捕获阶段,我描述给省略了。
咱们只给外层的ul绑定了事件处理函数,可是能够看到li点击的时候,实际上会打印出对应li的内容(1,2,3或者4)。 咱们无须给每个li绑定事件处理函数,不只从代码量仍是性能上都有必定程度的提高。
这个有趣的东西,咱们给了它一个好听的名字“事件代理”。在实际业务中咱们会常用到这个技巧,这同时也是面试的高频考点。
事件其实不是浏览器特有的,和JS语言也没有什么关系,这也是我为何没有将其划分到JS部分的缘由。不少地方都有事件系统,可是各类事件模型又不太一致。
咱们今天讲的是浏览器的事件模型,浏览器基于事件驱动,将不少东西都抽象为事件,好比用户交互,网络请求,页面加载,报错等,能够说事件是浏览器正常运行的基石。
咱们在使用的框架都对事件进行了不一样程度的封装和处理,除了了解原生的事件和原理,有时候了解一下框架自己对事件的处理也是颇有必要的。
当发生一个事件的时候,浏览器会初始化一个事件对象,而后将这个事件对象按照必定的逻辑进行传播,这个逻辑就是事件传播机制。 咱们提到了事件传播其实分为三个阶段,按照时间前后顺序分为捕获阶段,目标阶段和冒泡阶段。开发者能够选择监听不一样的阶段,从而达到本身想要的效果。
事件对象有不少属性和方法,容许你在事件处理函数中进行读取和操做,好比读取点击的坐标信息,阻止冒泡等。
最后咱们经过一个例子,说明了如何利用冒泡机制来实现事件代理
。
本文只是一个浏览器事件机制的科普文,并无也不会涉及到不少细节。但愿这篇文章能让你对浏览器时间有更深的理解,若是你对nodejs时间模型感兴趣,请期待个人nodejs事件模型。 事件循环和事件循环也有千丝万缕的联系,若是有时间,我会出一篇关于时间循环的文章。