如何利用文档碎片减少DOM操作
当我们想要改变页面中的元素时,会对DOM进行操作。例如我们想要在列表list
中插入新的一行li
,就会先新建一个元素const li= document.createElement('li')
,然后将它插入到它的父元素中list.appendChild(li)
,这样就完成了一次DOM操作。但是当我们需要插入更多的DOM元素时,进行多次的DOM操作会大大占用电脑的资源,从而降低网页运行的效率。那么我们应该怎样减少DOM操作的次数呢?
我们可以用到文档碎片DocumentFragment
来完成这一目的。我们需要先新建一个文档碎片const fragment = createDocumentFragment()
,然后将之前的插入DOM这一步从添加到父元素中修改为添加到文档碎片中,也就是fragment.appendChild(li)
,进行这一步操作之后,在页面上并不会显示我们新插入的元素,我们需要在最后将文档碎片再插入到父元素中list.appendChild(fragment)
。这样我们就利用文档碎片完成了DOM操作。这样做有什么好处呢?刚刚提到元素在插入文档碎片之后并不会显示在页面上,也就是说并没有占用渲染的资源,它们都还在内存当中。当我们将文档碎片插入父元素中之后,才会一次性渲染出来,这相当于只进行了一次DOM操作。
假设文档碎片中有N个新元素,如果不用文档碎片操作的话,我们需要进行N次DOM操作才能完成,非常的占用资源,而使用文档碎片之后,我们只需要进行一次DOM操作即可,这样就大大减少了DOM操作的次数。
以下是完整代码:
<ul id="list"></ul>
<script>
const list = document.getElementById('list')
const fragment = document.createDocumentFragment()
for(i = 1; i < 3; i++){
const item = document.createElement('li')
item.innerHTML = `项目${i}`
// list.appendChild(item)
fragment.appendChild(item)
}
list.appendChild(fragment)
</script>
事件冒泡
事件冒泡的机制
假设我们有一个列表ul
作为父元素,里面有几行li
作为子元素,当我们给父元素ul
绑定点击click
事件后,点击子元素li
,也会触发父元素的点击事件。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
function addEvent(target, event, fn){
target.addEventListener(event, fn)
}
const list = document.getElementById('list')
addEvent(list, 'click',()=>{
console.log('click');
})
</script>
如果我们给body
绑定点击事件,也同样会触发body
的点击事件,这就是事件冒泡,当我们触发一个事件时,会先去找这个元素上有没有绑定事件处理函数,如果有就执行,然后再继续找它的父元素有没有事件处理函数,如果有的话同样会执行,这样一直找父元素的事件处理函数并执行,直到找到body
上,这就是事件冒泡的机制。
如何利用事件冒泡
有时候,我们要给多个相同的元素绑定事件,我们就可以给这些元素的父元素绑定事件,这样当我们触发这些子元素的事件时,由于事件冒泡的特性,就会触发它们父元素上绑定的事件处理函数,从而实现这个需求。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
function addEvent(target, event, fn){
target.addEventListener(event, fn)
}
const list = document.getElementById('list')
addEvent(list, 'click',()=>{
console.log('click');
})
</script>
如何阻止事件冒泡
和上面一样的情况中,我们利用了事件冒泡给所有子元素绑定了同一个事件,但是有时候子元素中有与众不同的元素(比如list中的最后一个元素是一个按钮),不能和其他元素执行同一个的事件处理函数,那么我们必须单独给它绑定一个事件处理函数。但是这样还不够,由于事件冒泡的特性,当我们触发这个单独元素的事件时,还会触发它父元素的事件。于是我们需要阻止事件冒泡。
可以用event.stopPropagation()
来完成。首先我们需要给子元素的事件处理函数传入参数event
,然后再最开头加入这一段代码。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
<button id="btn">确定</button>
</ul>
<script>
function addEvent(target, event, fn){
target.addEventListener(event, fn)
}
const list = document.getElementById('list')
const btn = document.getElementById('btn')
addEvent(list, 'click',()=>{
console.log('click');
})
addEvent(btn, 'click', (event)=>{
event.stopPropagation()
console.log('click btn');
})
</script>
事件代理
假设我们有一个列表,里面有三行,分别为项目1、2、3,我们想要点击项目时,弹出信息显示当前点击的是哪个项目。可以这样做,先获取所有li
的DOM节点const lis = document.getElementsByTagName('li')
,因为通过getElementsByTagName()
获取到的是一个伪数组,不能被遍历,所以我们可以通过Array
类中的slice
来将其转化为可以被遍历的数组lisArray = Array.prototype.slice.call(lis)
,之后我们就可以用forEach()
来遍历它了,给所有的li
都绑定上事件处理函数。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
function addEvent(target, event, fn){
target.addEventListener(event, fn)
}
const list = document.getElementById('list')
const btn = document.getElementById('btn')
const lis = document.getElementsByTagName('li')
lisArray = Array.prototype.slice.call(lis)
lisArray.forEach(li => {
addEvent(li, 'click', ()=>{
alert(li.innerHTML)
})
})
</script>
但是这样做会出现两个问题。第一个是当我们的li
元素比较多时,会绑定很多很多个事件处理函数,每个函数都是要占用内存的,这样会极大地占用资源。第二个是如果这个列表是动态的,会根据情况增加新的li
,那么这个新的li
是不会被绑定事件的。
事件代理能做什么
对于上面提到的两个问题,这时候就可以用到事件代理,也可以叫做事件委托。可以利用之前提到的事件冒泡的机制,我们在它们的父元素ul
上绑定事件处理函数。但是我们还是想要展示当前点击的是哪个项目,这时候需要一个新的参数。我们可以利用事件处理对象e.target
来获取当前触发事件的元素节点。因为列表中还有一个添加新项目的按钮btn
,他不应该触发事件处理函数,所以我们可以使用if
进行判断元素节点的名称是否是LI
,即if(target.nodeName === 'LI')
,然后弹出点击的项目的信息alert(target.innerHTML)
。这样就利用事件冒泡的特性完成了给所有子元素的事件绑定,并且因为只绑定了一个事件处理函数,占用了较少的资源。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
<button id="btn">添加项目</button>
</ul>
<script>
function addEvent(target, event, fn){
target.addEventListener(event, fn)
}
const list = document.getElementById('list')
const btn = document.getElementById('btn')
const lis = document.getElementsByTagName('li')
lisArray = Array.prototype.slice.call(lis)
// lisArray.forEach(li => {
// addEvent(li, 'click', ()=>{
// alert(li.innerHTML)
// })
// })
addEvent(list, 'click', (e)=>{
const target = e.target
if(target.nodeName === 'LI'){
alert(target.innerHTML)
}
})
addEvent(btn, 'click', ()=>{
const li = document.createElement('li')
li.innerHTML = '新建项目'
list.insertBefore(li, btn)
})
</script>
如何封装绑定事件处理函数
如果我们想要封装一个函数来进行绑定事件处理函数的操作,并且让他能够进行事件代理。我们可以定义一个函数bindEvent()
,传入四个参数,分别是绑定对象target
,绑定事件event
,要代理的对象selector
,以及回调函数fn
,当我们传入三个参数时,直接进行回调函数的操作,但这时需要进行一个简单的赋值,即将第三个参数selector
的值给fn
,将selector
变为null
,代表此时不需要进行代理。当我们传入四个参数时,我们要进行对事件触发元素的判断,利用target.matches(selector)
可以完成,如果不是我们想要代理的元素则不进行任何操作。然后我们再进行回调函数fn
的调用。
<ul id="list">
<li id="li1">项目1</li>
<li>项目2</li>
<li>项目3</li>
<button id="btn">确定</button>
</ul>
<script>
function bindEvent(target, event, selector, fn){
if(fn === null){
fn = selector
selector = null
}
target.addEventListener(event, (e)=>{
if(selector){
const target = e.target
if(target.matches(selector)){
fn(e)
}
}else{
fn(e)
}
})
}
const list = document.getElementById('list')
const li1 = document.getElementById('li1')
bindEvent(li1, 'click', (e)=>{
alert(e.target.innerHTML)
} )
bindEvent(list, 'click', 'li', (e)=>{
alert(e.target.innerHTML)
})
</script>
在这段代码中我们只给第一个li
元素绑定了点击事件,但是点击其他的li
也会触发同样的事件,证明下面的事件代理也成功起到了作用。