对JS事件机制的深入理解
事件绑定方法有三种,如下:
- Html事件处理程序-直接在元素上指定事件及相应的处理程序,事件处理程序中可直接访问event对象(不需要用参数接收event独享),this指向当前元素,同时还扩展了this和document的作用域,即访问对象的属性或方法时可以省略this或document
- Dom0级事件处理程序-只支持冒泡阶段,同一事件如果指定多个处理程序,后面的会覆盖前面的。绑定非箭头函数时,内部this代表当前发生事件的元素。
- Dom2级事件处理程序-obj.addEventListener("click",func) 默认是冒泡阶段(将第三个参数为true则表示捕获阶段),可为同一元素指定多个事件处理程序。绑定非箭头函数时,内部this代表当前发生事件的元素。注意:IE8及之前使用attachEvent绑定事件处理程序,并且只支持冒泡阶段。
event对象及常用属性的兼容性:
- 标准浏览器通过事件处理程序的参数来获取,IE8及更低版本采用window.event来获取
- target和currentTarget的区别: target指向触发事件的元素,currentTarget指向事件绑定的元素,详情请看这篇
- event常用属性及方法的兼容性对比
行为 |
标准dom |
IE8及之前的浏览器 |
取消事件默认行为 |
e.preventDefault() |
e.returnValue=false |
阻止事件冒泡 |
e.stopPropagtion() |
e.cancelBubble=false |
获取事件目标 |
e.target |
e.srcElement |
注意:在事件处理函数中使用return false,并不能阻止事件冒泡,也不能取消事件默认行为。
二、事件的执行是异步的。
三、对事件队列的处理(放入及执行)是浏览器的渲染进程(又叫内核进程、render进程)负责,浏览器是一个多进程架构,如下图所示:
- 浏览器主进程:主要负责显示界面、提供前进后退收藏等交互行为,提供子进程管理功能、存储用户数据。
- GUP进程:GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程
- 网络进程:主要负责页面的网络资源加载。
- 插件进程:主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
- 渲染进程:又称之为浏览器内核,包含以下线程
- GUI线程
负责把html解析成dom tree,把css解析成css rule,然后把两者结合,形成render tree。然后计算出layout tree放入浏览器内存中供浏览器主进程显示到界面上;
当界面需要重绘或回流时,该线程会被执行;
该线程与JS引擎线程是互斥的,当JS引擎在执行时,该线程会处于暂停状态。这也就是为什么一段JS代码执行时间比较久时页面会出现空白或卡顿的原因
- JS引擎线程
又叫JS内核,负责解析和执行JS代码;
当JS引擎执行到事件代码语句时(比如addEventListener),会通知事件线程需要关注XXX事件;
- 事件触发线程
主要负责事件队列的维护
当事件触发时会被事件线程捕获到,事件线程把事件处理程序放入到事件队列中,这一过程不会打断JS引擎线程的执行
- 定时器触发线程
setTimeout与setInterval所在的线程;
由于js引擎是单线程的,如果处于阻塞线程状态,则会影响记时的准确性,因此需要单独的线程来计时并触发事件
当到达指定时间时,setTimeout回调代码也会被加入到事件队列,注意是加入事件队列,而不是立即执行,因为如果在队列中还有其他待执行的代码时就不会执行;
setInterval的情况更特殊,当到达指定时间时,如果上一次的回调函数还在队列中等待执行,则直接跳过而不做任何操作。
- 异步请求线程
XmlHttpRequest所在线程;
假如不采用单独的线程而是直接使用JS引擎线程,从发出请求到等待服务端给出响应的这段时间,JS引擎就会一直处于运行状态,原本可以继续执行后面的代码的,现在只能等待
JS是单线程运行的的、那Worker为何可以达到多线程的效果?
为什么JS是单线程执行的?假如JS是多线程执行的,一个线程要添加DOM、另一个线程要删除DOM,就会乱套,这就像两台电脑不能同时使用同一台打印机一样的道理;
创建WebWorker时,JS引擎线程向浏览器申请了worker专用的线程做为js引擎线程的子线程,两者通过postMessage Api进行线程间的通讯,可以理解为浏览器给JS引擎线程开了外挂;
ShardWorker是浏览器中所有tab标签共享的,每个tab标签都有独立的render进程,所以SharedWorker不属于某个Render进程,而是浏览器开了独立的进程来管理。
Event Loop
js执行是单线程的,为了能让UI 渲染、脚本执行、各种事件、网络请求等进行协同工作,将任务分成了同步任务和异步任务来避免阻塞问题,同步任务直接压入调用栈中等待js引擎所在线程执行,异步任务会在有了结果后将注册的回调函数放入到相应的任务队列,等待调用栈为空时去采用轮询机制去执行。
JavaScript将异步任务队列分为宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue),不同的异步任务会被放入不同的队列中。
- 宏任务:包括
setTimeout
、setInterval
、I/O操作。 - 微任务:包括
Promise
的回调、process.nextTick
(Node.js)、MutationObserver
。
事件循环的执行流程如下:
- 执行一个宏任务,检查是否有同步代码执行。
- 执行所有微任务,直到微任务队列为空。
- 如果有剩余的宏任务,取出第一个继续执行,并重复以上步骤。
注意:同步任务执行完成后才会开始执行异步任务
经典面试题:
async function async1(){ console.log("async1 start") await async2() console.log("async1 end") } async function async2(){ console.log("async2") } console.log("script start") setTimeout(function(){ console.log("settimeout") },0) new Promise(function(resolve){ console.log("promise") resolve() }).then(function(){ console.log("promise then") }) async1()
首先执行主线程代码,并且队列有先进先出的规则
1、‘打印script start’--
2、宏任务排队在宏任务队列中(settimeout)--
3、‘打印promise’--
4、微任务排队在微任务队列中(promise then)--
5、async1():'打印async1 start'--
6、'打印async2'--
7、'async1 end'在微任务中排队,因为console.log("async1 end")是在async2函数返回的promise的then语句中执行的--
8、'打印promise then'--
9、'打印async1 end'--
10、'打印settimeout'
1、‘打印script start’--
2、宏任务排队在宏任务队列中(settimeout)--
3、‘打印promise’--
4、微任务排队在微任务队列中(promise then)--
5、async1():'打印async1 start'--
6、'打印async2'--
7、'async1 end'在微任务中排队,因为console.log("async1 end")是在async2函数返回的promise的then语句中执行的--
8、'打印promise then'--
9、'打印async1 end'--
10、'打印settimeout'