关于前端的两个面试点:事件代理和任务队列
两个知识点,做业务可能用不到,面试一定会用上.
事件代理,冒泡,捕获
先说冒泡和捕获:
dom中的元素是一层一层的父子结构,从document到body,再到div,再到button之类的.
那么事件呢,先从最外面传递到最里面,这叫捕获.
到了最里面那个元素,然后反过来向外传递,这叫冒泡.
通常用的addEventListener,就是给某个元素加上了冒泡阶段的监听.
那么怎么在父元素上拦截掉这个事件呢?就需要添加捕获阶段的监听.
element.addEventListener(
'click',
function (e) {
e.preventDefault() //阻止默认事件
},
true
)
前两个参数就不解释了,第三个为true,就是捕获阶段监听,在捕获阶段可以调用方法阻止事件进一步往下传递,自然后续的冒泡也没有了.
做过android的可以用onInterceptTouchEvent来理解下.一回事.
再来说事件代理,也叫事件委托:
通过在父元素上加事件监听的方式,来处理子元素的事件.
好处是什么呢?
- 不用分别给每个子元素添加和删除事件.
- 子元素非常多时,可以提升性能.
- 后续动态添加的子元素,也可以触发这个事件.
贴段代码,自己理解:
传统方法:
<ul>
<li v-for="(item, index) in data" @click="handleClick(index)">
Click Me
</li>
</ul>
使用事件代理:
<ul @click="handleClick">
<li v-for="(item, index) in data" :data-index="index">
{{ item.text }}
</li>
</ul>
handleClick(e) {
if (e.target.nodeName.toLowerCase() === 'li') {
const index = parseInt(e.target.dataset.index)
this.doSomething(index)
}
},
vue中需要使用事件代理吗?
不需要.
事件代理的3个优势:
- vue的元素/元素销毁时会自动移除事件,不需要手动操作.
- v-for中绑定的都是同一个事件,除非有上千个,否则和事件代理性能差距不大.
- vue中已经自动完成.
js中的事件循环机制
同步任务和异步任务
js一开始是设计在浏览器上使用的,在浏览器环境下操作dom,这种UI操作必须是单线程的,否则会带来很复杂的同步问题.
但是由于部分IO操作比较慢,全部串行执行又会阻塞…
于是引入异步任务:
同步任务在主线程排队执行.
异步任务放入任务队列.
同步任务执行完后,从任务队列取出异步任务执行.
这种运行机制称之为事件循环(EventLoop).
EventLoop:不停的从任务队列中取出任务放到主线程.
- 选择一个异步任务,执行,
- 在浏览器环境下,此时会渲染页面,
- 回到步骤1,开始下一轮循环.
一个循环叫做一个tick.
这里存在一个问题:
虽然有任务队列,但是插入任务的时候只能放在尾端.
如果有紧急任务,可能执行时机会比想象中的晚很多,不利于视图的同步.
微任务和宏任务
js引擎在异步任务基础上又实现了微任务.提供微任务队列和产生微任务的方法.
在每个异步任务执行完后,将微任务队列全部执行完(也包括微任务执行过程中新产生的微任务).
以前的异步任务则称之为宏任务.
- 宏任务:setInterval, setTimeout, DOM事件, Ajax请求
- 微任务:Promise,async/await
这样如果在某次宏任务中,发起了异步任务(如网络请求),期望在请求结果回来后尽快得到使用,而不是排到异步任务队列的尾部,就可以使用微任务:
将数据处理回调加到微任务队列中,得以在当前周期内完成.
微任务机制在Vue中的应用:
vue使用字典来维护修改数据触发的watcher.使用watcher的id去重.每个周期统一执行一次watcher的run方法.
主要有两个优势:
- 去重:一个周期内,不管修改多少次数据,同一个watcher最终只触发一次更新.
- 合并:将一个周期内的多个组件修改在一起执行,配合虚拟dom,减小性能损失.
核心机制:nextTick
nextTick本质上是将传入的回调放入队列,同时在首次执行nextTick方法时,创建一个微任务来处该理队列.这个微任务会在本次周期最后被执行.
根据不同浏览器环境,使用不同机制来生成这个微任务.
使用nextTick更新dom:
- watcher更新放入字典中,使用nextTick统一执行字典中所有watcher的run方法,run方法中使用虚拟dom进行对比,并更新真实dom(注意:更新dom并不代表渲染dom).
- 由于更新dom的watcher在微任务中执行,为了能最快获取更新后的dom,vue也提供了nextTick方法给上层使用,只需在改变数据后执行nextTick,就能通过紧随其后的微任务获取到更新后的dom.
this.content = "改变后的" //触发watcher.update,对应watcher被加入队列,队列统一处理方法被加入微任务队列.
console.log("content改变前:",this.$refs.content.innerText)
this.$nextTick(() => {//继续加入微任务,由于在上面的微任务后面,因此可以得到更新后的dom
console.log("content改变后:",this.$refs.content.innerText)
})
注意:dom更新和UI渲染不是一回事.dom更新可以同步进行.UI渲染则要等到一次宏任务执行完毕.
一些常用回调和事件:
requestAnimationFrame
浏览器在下次渲染(UI render)之前调用指定的回调函数更新动画。 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次渲染之前执行
渲染(UI render)
一轮事件循环执行结束之后,下轮事件循环执行之前开始进行 UI render。 即:macro-task 任务执行完毕,接着执行完所有的 micro-task 任务后, 此时本轮循环结束,如果要执行 UI render,先调用requestAnimationFrame, 然后执行 UI render。UI render 完毕之后接着进行下一轮循环;
requestIdleCallback
requestIdleCallback 会在每次 检查是否要渲染(check) 结束, 发现距离下一帧的刷新还有时间, 就执行一下这个。如果时间不够,就下一帧再说。
总结:
- 异步任务都是通过队列来完成.不止js,几乎所有GUI程序的渲染主进程都是这样.
- 为了插队,引入异步任务中的微任务.
- 宏任务是宿主环境创建的任务,微任务是js引擎创建的任务.
- js引擎将微任务队列中的任务放在某个循环周期内一次执行完成,提供更加灵活的任务调度机制.
- vue通过队列将dom更新事件去重与合并,并通过微任务在下次渲染前执行.
- micro-task 结束 -> requestAnimationFrame -> requestIdleCallback -> UI render
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!