事件是用户与界面交互的基础。
什么是事件驱动
JavaScript是一种事件驱动语言。那么到底什么是事件驱动呢?
事件驱动编程是一种编程范式。在事件驱动这种范式中,执行什么操作是由事件来决定的,比如用户点击事件,其他编程线程事件,数据库请求事件。事件的处理由事件处理函数或回调函数来完成。
Event callback is a function that is invoked when something significant happens like when click event is performed by user or the result of database query is available.
事件回调函数由某个特定的事件来触发,比如用户点击或者请求数据库数据。
I/O编程 VS 事件驱动编程
I/O编程规定事件在某个进程结束之后执行。我们来看一组对比:
1 | // I/O编程 |
在I/O编程中,myResult
函数的执行需要等数据库请求这一进程结束之后才会执行。
1 | // 事件驱动编程 |
在事件驱动编程中,先定义在数据库请求结束之后要进行什么操作,并将这个操作存储在一个叫做query_finished
的函数中,然后将这个函数作为参数传递给数据库请求这一事件。当数据库请求结束时,就会自动触发query_finished
函数,而不是仅仅返回请求结果。
像这种通过定义在特定事件发生时触发特定函数,而非仅仅返回一个结果值的编程风格就叫做事件驱动或异步编程。这种编程风格在执行I/O时,当前进程不会发生阻塞,且可以多个I/O同时进行,每一个I/O的回调函数都会在操作结束时被触发。
可以把事件驱动联想成一个联动装置,一个动作会触发另一个动作,即一个事件触发一个函数。
事件循环 Event Loop
和事件驱动相辅相成的是事件循环机制。事件循环机制是在不断的循环过程中实现两大功能:
- 事件监测
- 事件处理器触发
事件循环机制监测任意时间内是否有某个事件发生,如果有,则需要触发相应的事件回调函数。
JavaScript的事件循环是一个大的话题,这里我们简单梳理一下:
浏览器是多进程的
浏览器是多线程运行的,每打开一个tab标签页,都会有独立的进程被创建出来。如果我们打开任务管理器就会发现,浏览器中每个tab也都有一个独立的进程,此外浏览器还有一个主进程。
浏览器包含哪些进程
- Browser进程 是浏览器的主进程(有且只有一个)。负责浏览器界面显示、页面管理、网络资源管理等
- 第三方插件进程 每种类型的插件对应一个进程(仅当使用时才创建)
- GPU进程 最多一个,用于3D绘制等
- 浏览器渲染进程 是浏览器内核,默认每个Tab页面一个进程,用于页面渲染、脚本执行、事件处理等
浏览器渲染进程
浏览器渲染进程是浏览器内核,页面渲染、JS的执行、事件的循环都在这个进程中进行。渲染进程是多线程的。渲染进程中包括以下线程:
- GUI渲染线程 负责渲染浏览器界面(解析HTML、CSS等)、重绘和回流,它与JS引擎线程互斥
- JS引擎线程 负责处理JS脚本程序
- 事件触发线程 用来控制时间循环
- 定时触发器线程
setTimeout
和setInterval
所在线程 - 异步http请求线程
其中,JS引擎线程用于处理任务,所有的同步任务都在这个线程上执行,形成一个执行栈。
在这个线程之外,还有一个“任务队列”,其他线程的异步任务在满足触发条件后,就添加到“任务队列”中,表示这个事件可以进入“执行栈”了。主线程在空闲时会从“任务队列”中读取事件,它执行这个事件的方式就是执行这一事件对应的回调函数。
“任务队列”是一个先进先出的数据结构,排在前面的事件优先被读取,主线程的读取过程是自动的,只要执行栈一清空,“任务队列”上的第一位事件就自动进入主线程。
主线程的这一不断执行——读取“任务队列”——执行的运行机制成为Event Loop(事件循环)。
JS的多线程:Web Worker
事件循环机制意味着如果页面中有大量的同步任务在执行,那么事件函数很可能需要长时间在消息队列中,无法被执行,导致页面看起来很迟钝,影响使用体验。
如果真的遇到这样的问题,浏览器提供了一个新的解决方案,叫做Web Workers。一个Worker就是一个独立于主程序的JS进程。
MDN的官方解释是:
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面;
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件, 这个文件包含将在工作线程中运行的代码;
workers 运行在另一个全局上下文中,不同于当前的window;
因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误
简单来说,
- 创建Worker时,JS引擎向浏览器申请创建一个子线程(完全受主线程控制,且不能操作DOM)
- JS引擎线程与worker线程之间通过特定的方式通信
比如我们需要一个独立的线程来帮我们作平方数运算,由于平方运算非常耗时,所以我们在一个叫做”squareworker.js”的文件中来写代码。
为了避免worker线程和其他线程同时操作相同的数据导致错误发生,workers线程不与主线程环境共享它的全局环境和数据,而是利用信息往返来实现数据交换。
1 | // squareworker.js中输出结果数据 |
我们可以尝试在浏览器中打开(会存在兼容性问题,Chrome需要下载subworker.js文件支持,火狐可以正常打开)
事件流
事件流指从页面中接收事件的顺序,也可以理解为事件在页面中传播的顺序。
背景
早期的IE事件传播方向是从上至下,即从document逐级向下传播到模板元素;而Netscape Navigator则是相反的顺序,从目标元素开始向上逐级传播至window。
后来ECMAScript在DOM2中对事件流进行了进一步规范,基本上是上述两者的结合。事件流要经历三个阶段:
事件捕获
当事件发生时,document对象会发出一个事件流,由上至下逐级DOM节点传播,直到到达触发事件的目标元素为止,这个过程叫做事件捕获。
事件处理
到达目标元素之后,执行目标元素绑定的处理函数。如果目标元素没有绑定监听函数,则不做任何处理。
事件冒泡
事件流从目标元素开始,向最外层的DOM节点逐级依次传递,途中如果有节点绑定了事件处理函数,这些函数会被执行。
对于事件处理器来说,可以调用stopPropagation
方法来取消传播,从而阻止目标对象的外层元素的事件处理器被触发。比如当一个按钮被包含在另一个可点击元素内,我们不希望点击按钮触发外层元素的点击事件时,就可以使用取消冒泡的方法。
所以捕获先于冒泡执行。
事件委托
通过事件对象的target
属性和冒泡过程,我们可以给父元素添加事件监听器,来监听和处理子元素的事件,避免重复为子元素绑定相同的事件。当目标元素被触发时,这个事件会向外传递,冒泡到父元素上,触发父元素绑定的事件函数,父元素可以通过event.target
获取到被触发的目标元素。
比如有一个node节点包含一串button
元素,点击按钮会触发相同的事件,那么给这个node节点绑定点击事件就会比给每一个button
都绑定点击事件方便得多。而通过event.target
即可得知哪一个button
被点击了。
1 | <button>A</button> |
事件对象 Event Object
当我们使用事件处理函数时,实际上我们传递了一个参数:事件对象(event object)。这个对象包含了关于事件的其他信息,这些信息因事件类型的不同而不同。
事件对象只在事件发生时产生,并且只能在事件处理函数内部访问,在所有事件处理函数执行结束后,事件对象就被销毁。
事件对象的属性
bubbles
返回布尔值,表示事件是否是冒泡事件类型。
cancelable
返回布尔值,表示事件是否拥有可取消的默认动作。
target
大多数事件对象都有target
属性,指的是触发事件的目标节点。我们可以通过这个属性来确保触发的是目标元素,而不是冒泡过程中意外触发的其他节点。
type
返回当前事件对象表示的事件的名称。
事件对象的方法
preventDefault()
通知浏览器不要执行与事件关联的默认动作。
很多事件都有自己的默认动作,比如点击链接会自动跳转到链接的目标地址,点击右键会自动出现右键菜单。对于大多数事件类型来说,事件处理器会在默认动作前被调用,但是如果不希望默认动作出现,则可以使用preventDefault()
事件来阻止默认动作触发。
但是建议只有在必需的时候使用这一方法,因为破坏用户习惯的默认动作会带来不好的使用体验。此外,在某些浏览器上,有些事件的默认动作无法被完全屏蔽,比如在chrome上,关闭当前标签页的快捷键就无法被改变。
stopPropagation()
不要派发事件。
兼容性问题
事件对象也存在一些兼容性问题。
事件对象
IE8之前,注册事件处理函数时,事件对象并没有被传递到函数内部,因此需要使用window.event
来获取事件:
1 | function getEvent(event) { |
preventDefault()
IE没有preventDefault()
方法,所以我们需要设置的是:
1 | window.event.returnValue = false; |
stopPropagation()
IE也没有stopPropagation()
方法,所以需要设置cancelBubble
属性
1 | event.cancelBubble = true; |
事件处理
有个三种方式来给元素添加事件:
- Inline event handlers 内联事件处理器
- Event handler properties 事件处理属性
- Event listeners 事件监听器
内联事件处理器
内联事件处理器,也称为HTML事件处理程序,是指直接给元素添加事件属性,事件属性的属性值为事件函数:
1 | <button onclick="changeText()"> |
当我们打开页面时:
当我们点击按钮后:
内联事件处理器是最直接的理解事件的方式,但是在实际编程过程中,除了测试和教学目的,很少真正使用它们。
内联事件处理器就和CSS内联样式一样,不易于代码维护,所以不建议使用。
事件处理属性(DOM0级事件处理程序)
事件处理属性的触发机制和内联事件处理器类似,只不过我们通过JavaScript来设置元素的属性,而不是直接在HTML中设置。
1 | <button>Click me!</button> |
这里需要注意两个细节:
- 通过JavaScript来添加
onclick
属性时,onclick
并不用遵循驼峰命名法写成onClick
- 给
onclick
属性添加函数作为属性值时,不需要添加括号()
,因为我们仅将函数传递给属性,而不是在此时触发函数
通过button.onclick = null
可以删除事件处理程序。
通过事件处理属性来添加函数比内联处理器要易于维护,但它同样有一些缺陷,比如它无法传递多个事件处理函数给同一个事件,如果给点击事件赋上多个onclick
属性值,后一个事件处理函数会覆盖前一个函数。此外,这种方法也不能控制事件流到底是捕获还是冒泡。
事件监听器
事件监听器用于监听元素上的事件,我们通过addEventListener()
方法来给监听事件。
1 | <button>Click me!</button> |
addEventListener()
接受三个参数:
- 要处理的事件名(不需要添加前缀
on
) - 作为事件处理程序的函数
- 一个Boolean值,默认false表示使用冒泡机制,true表示使用捕获机制
addEventListener()
有以下优势:
addEventListener()
可以给同一个元素的同一个事件添加多个函数(同一个事件触发同一个函数的情况,添加数次只生效一次),触发顺序即添加顺序- 可以给事件添加匿名函数
- 可以通过
removeEventListener()
来移除所有事件 - 可以给
document
和window
对象来添加事件
因此,addEventListener()
是目前JavaScript中使用最广泛的事件处理方法。
IE处理方法
IE9之前都不支持addEventListener()
方法,而是使用两个相似的方法:
attachEvent()
添加事件处理器detachEvent()
删除事件处理器
它们都接受两个参数,第一个是事件处理程序的名称(不是事件,所以要添加on
前缀);第二个是事件处理函数;之所以没有第三个参数是因为IE8之前只支持冒泡事件流。
由于支持的是冒泡事件流,如果给元素绑定多个事件处理函数,事件触发的顺序是添加顺序的相反顺序。
attachEvent
有一个缺点,不同于addEventListener()
的this
值指向目标元素,attachEvent()
的this
值指向window
对象。
跨浏览器的写法
1 | let EventUtil = { |
常见事件
鼠标事件
Event | Description |
---|---|
click | 鼠标点击触发 |
dbclick | 鼠标双击触发 |
mouseenter | 鼠标进入元素触发 |
mouseleave | 鼠标移出元素触发 |
mousemove | 每一次鼠标在元素内移动触发 |
click
点击事件包括了mousedown
和mouseup
事件。
表单事件
Event | Description |
---|---|
submit | 表单提交时触发 |
focus | 某个元素获得焦点时触发 |
blur | 元素失去焦点时触发 |
键盘事件
Event | Description |
---|---|
keydown | 当某个键被按下时触发 |
keyup | 当某个键被松开时触发 |
keypress | 当某个字符键被按时触发(非字符键如alt ,shift ,ctrl ) |
键盘事件是一种特殊的事件,它触发的事件对象能够返回按键的相关属性,如keyCode
,key
,code
。我们以字母a
为例:
Property | Description | Example |
---|---|---|
keyCode |
按键对应的键码值 | 65 |
key |
按键名 | a |
code |
按键的Unicode字符 | KeyA |
触摸事件
Event | Description |
---|---|
touchstart | 当一个手指开始触碰屏幕 |
touchmove | 当一个手指在屏幕上移动 |
touchend | 当手指离开屏幕 |
手势事件
Event | Description |
---|---|
gesturestart | 两只手指触碰屏幕 |
gesturechange | 任意手指移动 |
gestureend | 某一只手指离开屏幕,只剩一只手指在屏幕上 |
触发手势事件的时候也会触发触摸事件。
滚动事件
Event | Description |
---|---|
scroll | 当元素开始滚动 |
参考:
Introduction to Event-Driven Programming
Understanding Events in JavaScript