【前端知识整理】JS中的DOM操作 事件绑定 事件冒泡 事件代理

如何利用文档碎片减少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也会触发同样的事件,证明下面的事件代理也成功起到了作用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值