对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引擎所在线程执行,异步任务会在有了结果后将注册的回调函数放入消息队列中,等待调用栈为空时去采用轮询机制去执行。
异步任务分为宏任务和微任务,宏任务和微任务各有一个队列,在单次循环中,先执行完所有的微任务队列,然后再执行一个宏任务。(注,宏任务和微任务谁先执行,不同的浏览器不同的,浏览器端和node端也不同,能得出的共同结论就是单次循环中,只执行一个宏任务,而所有微任务都会被执行完)
宏任务对时间精度要求不高,常见的宏任务有setTimeout,setInterval,setImmediate,requestAnimationFrame,键盘事件、鼠标事件、xmlhttprequest请求等
微任务就不同了,微任务必须在下一次渲染前完成,要尽可能的块,常见的微任务有Promises,Object.observe,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'



posted @ 2019-07-17 17:39  我是格鲁特  阅读(1836)  评论(0编辑  收藏  举报