JavaScript之DOM与事件精讲

DOM选择器

DOM是文档对象模型。DOM用一个逻辑树来表示一个文档,树的每个分支终点都是一个节点,每个节点都包含着对象。

DOM选择器,用于快速定位DOM元素。在原生JS中,有提供根据id、name等属性来查找的传统选择器,也有新型的、更高效的querySelector选择器和querySelectorAll选择器。

原生JS选择器定位元素:

  • getElementById
  • getElementByClassName
  • getElementsByName
  • getElementByTagName

以上几种选择器不再赘述。

定位到特定元素的子元素,于是诞生了新型的querySelector选择器和querySelectorAll选择器。

1.querySelector选择器返回的是匹配到的基准元素的第一个子元素。该基准元素可以为document,也可以为基本的Element。

<body>
    <div>
        <h5>Original content</h5>
        <span>outside span</span>
        <p class="content">
            inside paragraph
            <span>inside span</span>
            inside paragraph
        </p>
    </div>
</body>
<script>
    var baseElement=document.querySelector("p"); 
    //代码输出的结果是”inside span“  
    console.log(baseElement.querySelector("div span").innerText);
</script>

第一行代码获取的是基准元素为p元素,第二行代码中的选择器为”div span“。虽然在p元素中没有div元素,却依旧能匹配到span元素。这是因为在匹配过程会优化找出div元素下span元素的集合,然后判断span元素是否属于p元素的子元素,最后返回第一个匹配到的span元素值。

2.querySelectorAll选择器会返回基准元素下匹配到的所有子元素的集合。

通过一个例子来理解querySelectorAll选择器匹配元素的过程。

<body>
    <div id="my-id">
        <img id="inside">
        <div class="lonely"></div>
        <div class="outer">
            <div class="inner"></div>
        </div>
    </div>
</body>
<script>
    var firstArr=document.querySelectorAll('#my-id div div');
    var secondArr=document.querySelector('#my-id').querySelectorAll('div div');
    console.log(firstArr);
    console.log(secondArr);
</script>

上面代码的主要目的是找出id为”my-id" 元素的子div中子div元素的集合。
但是最后结果却不一样。
在这里插入图片描述
针对firstArr,querySelectorAll选择器的调用方是document,则基准元素为document,执行css选择器,匹配到的元素只有一个。
而secondArr,先通过querySelector选择器确定基准元素是id为“my-id"的元素,然后执行css选择器,选择器的内容是匹配div元素中的子div元素,还是很好理解的。

HTMLCollection对象与NodeList对象

<body>
    <div id="main">
        <p class="first">first</p>
        <p class="second">second<span>content</span></p>
    </div>
</body>
<script>
    var main=document.getElementById("main");
    console.log(main.children);
    console.log(main.childNodes);
</script>

结果如下:
在这里插入图片描述

HTMLCollection对象

HTMLCollection对象具有length属性,返回集合的长度,可以通过item()函数和namedItem()函数来访问特定的元素。

1.item()函数通过序号来获取特定的某个节点,超过索引则返回null。
2.namedeItem()函数用来返回一个节点,首先通过id属性去匹配,如果没有匹配到则使用name属性匹配,还没匹配到就返回null。

NodeList对象

同样也具有length属性,也具有item函数,基本与HTMLCollection一致,但是没有namedItem()函数。

两者异同

1.两者都具有实时性,对DOM树新增或删除一个节点,都会立刻在其中反映出来。
2.两者都是类数组结构,无法调用数组的函数。但可以通过call()函数和apply()函数处理为真正的数组后,就转变成静态值了,也不会动态的反映DOM的变化。
3.并不是所有的函数获取的NodeList对象都是实时的,例如通过querySelectorAll()函数获取到的NodeList对象就不是实时的。
4.HTMLCollection对象只包括元素的集合,就是具有标签名的元素,而NodeList对象是节点的集合,既包括元素,也包括节点。

<body>
    <ul id="main">
        <li>文本1</li>
        <li>文本2</li>
        <li>文本3</li>
        <li>文本4</li>
        <li>文本5</li>
    </ul>
</body>
<script>
    //获取ul
    var main=document.getElementById('main');
    //获取li集合
    var lis=document.querySelectorAll('ul li');
    //第一次输出li集合长度,值为5
    console.log(lis.length);
    //新增li元素
    var newLi=document.createElement('li');
    var text=document.createTextNode('文本8');
    newLi.appendChild(text);
    main.appendChild(newLi);
    //再次输出li集合长度,值为5
    console.log(lis.length);
    //重新获取li的集合并输出长度,值为6
    console.log(document.querySelectorAll('ul li').length);
    
</script>

常用的DOM操作

常用的DOM操作需要掌握,DOM操作是对文档结构中节点的操作,主要节点类型有元素节点属性节点文本节点
1.元素节点就是拥有一对开闭合标签的元素整体,如div、ul、li。
2.属性节点就是元素节点具有的属性,如a标签的href属性。
3.文本节点就是DOM中呈现文本内容得节点。
元素节点仅仅与文本节点存在父子关系。

新增节点

<body>
    <ul id="container">
        <li class="first">文本1</li>
        <li class="second">文本2</li>
    </ul>
</body>
<script>
    //获取指定元素
    var container=document.querySelector('#container');
    //新创建一个元素节点
    var newLiOne=document.createElement('li');
    //新创建一个属性节点
    var newLiAttr=document.createAttribute('class');
    //设置属性值
    newLiAttr.value='last';
    //新创建一个文本节点
    var newTextOne=document.createTextNode('新增文本1');
    //将文本节点作为元素节点得子元素
    newLiOne.appendChild(newTextOne);
    //将新增元素节点添加至末尾
    container.appendChild(newLiOne);
    //新增第2个节点
    var newLiTwo=document.createElement('li');
    newLiTwo.setAttribute('class','before');
    var newTextTwo=document.createTextNode('新增文本2');
    newLiTwo.appendChild(newTextTwo);
    //添加至第一个新增节点得前面
    container.insertBefore(newLiTwo,newLiOne);
</script>

删除节点

删除节点
removeChild()
删除属性
removeAttribute()

<script>
    var main=document.querySelector('#main')
    //获取最后一个节点
    var lastChild=main.lastElementChild;
    //获取文本节点
    var textNode=lastChild.childNodes[0];
    //删除文本节点
    lastChild.removeChild(textNode);
    //也可以用这种方式
    lastChild.innerHTML='';
</script>

事件流

JS和html之间的交互是通过事件实现的,常用的事件有鼠标单机click,加载load,鼠标指针悬浮mouseover。

一个页面会绑定很多的事件,具体的事件触发顺序是什么样的呢?

这就涉及到事件流,事件流描述的是从页面接收事件的顺序,事件发生后会在目标节点和根节点之间按照特定的顺序传播。

一个完整的事件流包含了3个阶段:事件捕获阶段—>事件目标阶段—>事件冒泡阶段

1.事件捕获阶段表现在不同的节点先接收事件,然后逐级向下传播,最具体的节点最后接受到事件。
2.事件目标阶段表示事件刚好传播到用户产生行为的元素上,可能是事件捕获的最后一个阶段,也可能是事件冒泡的第一个和阶段。
3.事件冒泡阶段表现在最具体的元素先接收事件,然后逐级向下传播,不具体的事件最后接受事件。

<body>
    <table border="1">
        <tbody>
            <tr>
                <td>这是td的元素</td>
            </tr>
        </tbody>
    </table>
</body>
<script>
    var table=document.querySelector('table');
    var tbody=document.querySelector('tbody');
    var tr=document.querySelector('tr');
    var td=document.querySelector('td');
     
     table.addEventListener('click',function(){
        console.log('table触发');
     });
    
     tbody.addEventListener('click',function(){
         console.log('tbody触发');
     });
     tr.addEventListener('click',function(){
         console.log('tr触发');
     });
     td.addEventListener('click',function(){
         console.log('td触发');
     });
</script>

点击td元素,有如下结果。【addEventListener()函数第三个参数默认为false,按照冒泡型事件处理】
在这里插入图片描述

<script>
    var table=document.querySelector('table');
    var tbody=document.querySelector('tbody');
    var tr=document.querySelector('tr');
    var td=document.querySelector('td');

    table.addEventListener('click',function(){
        console.log('table触发');
    },true);
    
    tbody.addEventListener('click',function(){
        console.log('tbody触发');
    },true);
    tr.addEventListener('click',function(){
        console.log('tr触发');
    },true);
    td.addEventListener('click',function(){
        console.log('td触发');
    },true);
</script>

点击td元素【addEventListener()第三个参数为true时为捕获型事件流】
在这里插入图片描述
修改其中任意两种组成不同的模式达到混合型事件流。

<script>
    var table=document.querySelector('table');
    var tbody=document.querySelector('tbody');
    var tr=document.querySelector('tr');
    var td=document.querySelector('td');

    //事件捕获
    table.addEventListener('click',function(){
        console.log('table触发');
    },true);
    //事件冒泡
    tbody.addEventListener('click',function(){
        console.log('tbody触发');
    },false);
    //事件捕获
    tr.addEventListener('click',function(){
        console.log('tr触发');
    },true);
    //事件冒泡
    td.addEventListener('click',function(){
        console.log('td触发');
    },false);

</script>

在这里插入图片描述
捕获类型事件会优于冒泡类型事件,整个事件流执行过程如下:
1.事件捕获阶段,从table元素开始,绑定的是捕获类型事件,最先执行。
2.事件捕获阶段,到tbody元素,绑定的是冒泡类型事件,跳过。
3.事件捕获阶段,到tr元素,绑定的是捕获类型事件,执行。
4.事件目标阶段,td元素触发目标元素事件,不论什么类型的事件,都会执行。
5.事件冒泡阶段,执行tr元素,绑定的是捕获类型事件,跳过。
6.事件冒泡阶段,tbody元素绑定的是冒泡类型事件,执行。
7.事件冒泡阶段,table元素绑定的是捕获类型事件,跳过。

事件处理程序

事件处理程序分为DOM0,DOM2,DOM3这三种级别,没有DOM1哦。

DOM0级事件处理程序

将一个函数赋值给另一个事件处理属性,有两种表现形式。
1.通过JS获取DOM元素,再将函数赋值给对应得事件属性。

var btn=document.getElementById("btn");
btn.onclick=function(){}

2.直接在html中设置对应事件属性的值,值又有两种表现形式,一种是执行的函数体,另一种是函数名,需要在script标签中定义该函数。

<button onclick="alert('hello');">点击</button>


<button onclick="clickFn()">点击</button>
<script>
  function clickFn(){
        alert('hello');
    }
</script> 

若以上两种同时存在,第一种会覆盖第二种,JS绑定事件处理程序得优先级高于在HTML中定义的事件处理程序。

DOM2级事件处理程序

DOM2级事件处理程序中,支持对同一个事件绑定多个处理函数。

    var warp=document.getElementById('wrap');

    wrap.addEventListener('click',function(){
        console.log('123');
    },false);

    wrap.addEventListener('click',function(){
        console.log('456');
    },false);

在需要删除绑定的事件时,不能删除匿名函数,添加和删除必须是同一个函数。

    var warp=document.getElementById('wrap');
    var handle=function(){
         console.log('789');
     }
     //是同一个函数,可以取消绑定的事件
     warp.addEventListener('click',handle,false);
     wrap.removeEventListener('click',handle);
     //匿名函数无法取消绑定事件
     wrap.addEventListener('click',function(){
        console.log('456');
    },false);
    wrap.removeEventListener('click',function(){});

DOM3级事件处理程序

最大特点就是允许自定义事件【由createEvent函数创建,可以接受四个参数,返回对象有一个initCustomEvent()函数】
createEvent函数有四个参数
1.type【触发的事件类型】
2.bubble【事件是否可以冒泡】
3.cancelable【事件是否可以取消】
4.detail【任意值,保存在event对象的detail属性中】

 <div id="watchDiv">监听自定义事件的div元素</div>
 <button id="btn2">单机触发自定义事件</button>
 
<script>
    var customEvent;
    //创建自定义事件
    (function(){
        //判断浏览器是否支持DOM3
        if(document.implementation.hasFeature('CustomEvents','3.0')){
            var detailData={name:'li'};
            customEvent=document.createEvent('CustomEvent');
            customEvent.initCustomEvent('myEvent',true,false,detailData);         
        }
    })();
    //监听自定义事件
    //获取元素
    var div=document.querySelector('#watchDiv');
    //监听myEvent事件
    div.addEventListener('myEvent',function(e){
        console.log('div监听到自定义事件的执行,携带的参数为:',e.detail);
    })
    var btn2=document.querySelector('#btn2');
    //绑定click事件,触发自定义事件
    btn2.addEventListener('click',function(){
        div.dispatchEvent(customEvent);
    })
</script>

Event对象

事件在浏览器中是以Event对象存在的,每触发一个事件,就会产生一个Event对象。

获取Event对象

1.作为参数传入,参数名为event
2.通过window.event属性获取

    var btn=document.querySelector('#btn');
    btn3.addEventListener('click',function(event){
        //event作为参数传入
        console.log(event);
        //通过window.event获取
        var winEvent=window.event;
        console.log(winEvent);
        //判断两种方式获取的event是否相同
        console.log(event==winEvent); //true
    })

但是鉴于不同浏览器具有差异性,有的浏览器可能不支持第二种方式,所以要实现兼容。

  var EventUtil={
        getEvent:function(event){
            return event||window.event;
        }
    }

获取事件的目标元素

  var EventUtil={
        getTarget:function(event){
            return event.target||event.srcElement;
        }
    }

target属性与currentTarget属性

target属性在事件目标阶段,真实操作的目标元素。
currentTarget是在事件捕获、事件目标、事件冒泡这3个阶段,当前事件流所处某个阶段对应的目标元素。

    <table border="1">
        <tbody>
            <tr>
                <td>这是td的元素</td>

            </tr>
        </tbody>
    </table>
   <script>
       function getTargetAndCurrentTarget(event,stage){
        var event=EventUtil.getEvent(event);
        var stageStr;
        if(stage==='bubble'){
            stageStr='事件冒泡阶段';
        }else if(stage==='capture'){
            stageStr='事件捕获阶段';
        }else{
            stageStr='事件目标阶段';
        }
        console.log(stageStr,
        'target:'+event.target.tagName.toLowerCase(),
        'currentTarget:'+event.currentTarget.tagName.toLowerCase()
        );
    }
    
    var table=document.querySelector('table');
    var tbody=document.querySelector('tbody');
    var tr=document.querySelector('tr');
    var td=document.querySelector('td');
    
          table.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'capture');
        },true);

        tbody.addEventListener('click',function(event){ 
            getTargetAndCurrentTarget(event,'capture');
        },true);

        tr.addEventListener('click',function(event){    
            getTargetAndCurrentTarget(event,'capture');
        },true);

        td.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'target');
        },true);

        
        table.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'bubble');
        },false);

        tbody.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'bubble');
        },false);

        tr.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'bubble');
        },false);

        td.addEventListener('click',function(event){
            getTargetAndCurrentTarget(event,'target');
        },false);
   </script>

在这里插入图片描述

阻止事件冒泡

event.stopPropagation();

事件委托

利用事件冒泡原理,利用父元素来代表子元素的某一类型的事件的处理方式。
假如有ul标签有1000个子标签,单机li,输出li的文本内容。
传统方法是获取所有li标签,遍历一一添加事件。

    <ul>
        <li>文本1</li>
        <li>文本2</li>
        <li>文本3</li>
        <li>文本4</li>
        <li>文本5</li>
        <li>文本6</li>
    </ul>
    <script>
    
     var children=document.querySelectorAll('li');

     for(var i=0;i<children.length;i++){
         children[i].addEventListener('click',function(){
            console.log(this.innerHTML);
         });
     }
    </script>

但是这种方法不是很好,对浏览器的性能是一个很大的挑战。
1.事件处理程序过多导致页面交互事件较长。
事件处理程序需要不断的与DOM节点进行交互,因此引起浏览器重绘和重排的次数也会增多,从而延长页面交互时间

2.事件处理程序过多导致内存占用过多。
在JS中,一个事件处理程序就是一个函数对象,会占用一定的内存空间。假如页面有10000个li标签,则会产生10000个函数对象,占用的内存空间会急剧上升,从而影响浏览器的性能。

利用事件冒泡原理,当事件进入冒泡阶段时,通过绑定在父元素上的事件对象来判断当前事件流正在进行的元素,如果和期望的元素相同,就执行相应的事件代码。

     //为了支持不同浏览器,实现兼容
    var EventUtil={
        getEvent:function(event){
            return event||window.event;
        },
        getTarget:function(event){
            return event.target||event.srcElement;
        }
    }

    //1.获取父元素
    var parent=document.querySelector('ul');
    //父元素绑定事件
    parent.addEventListener('click',function(event){
        //获取事件对象
        var event=EventUtil.getEvent(event);
        //判断当前事件流所处的元素
        var target=EventUtil.getTarget(event);
        //与目标元素相同,做对应的处理
        if(target.nodeName.toLowerCase()==='li'){
            console.log(target.innerText);
        }
    })

事件是绑在父元素ul上的,不管子元素li有多少个,不会影响页面事件处理程序的个数,因此可以极大地提高浏览器的性能。

同一个ul下的所有li操作都一样,如果针对不同的元素所做的处理不一样,事件委托能否处理吗?

    <div id="box">
        <input type="button" id="add" value="新增"/>
        <input type="button" id="remove" value="删除"/>
        <input type="button" id="update" value="修改"/>
        <input type="button" id="search" value="查询"/>
    </div>
    <script>
    
            //为了支持不同浏览器,实现兼容
    var EventUtil={
        getEvent:function(event){
            return event||window.event;
        },

        getTarget:function(event){
            return event.target||event.srcElement;
        }
    }

    var parent=document.querySelector('#box');
    parent.addEventListener('click',function(event){
        var event=EventUtil.getEvent(event);
        var target=EventUtil.getTarget(event);
        switch(target.id){
            case 'add':
                console.log('新增');
                break;
            case 'remove':
                console.log('删除');
                break;
            case 'update':
                console.log('修改');
                break;
            case 'search':
                console.log('查询');
                break;
        }
    })
</script>

文档加载完成事件

在DOM中,文档加载完成有两个事件,一个是load事件,一个是ready事件。

  • ready事件的触发表示文档结构已经加载完成,不包含图片、flash等非文字媒体内容。
  • load事件的触发表示页面中包含的图片、flash等所有元素都加载完成。

load事件有两种实现方式:

<body onload="bodyLoad()">
</body>
<script>
    function bodyLoad(){
      console.log('文档加载完成,执行onload方法');
   }
</script>
   <script>
     window.onload=function(){
        console.log('文档加载完成,执行onload方法');
     }
   <script>

对于一个图片网站来说,如果我们需要等到所有的图片都加载完成再去执行相应的操作,这将会用户带来一段很长的等待时间,图片的加载相比于普通的HTML元素会消耗更长的时间。

使用img标签加载完成后就可以执行初始化操作,而不需要等到img标签的src属性完全加载出来。这样将节省很长的等待时间,对性能来说是一大提升。

但是,ready事件并不是原生JS实现的,而是在jQuery中实现的,ready事件挂载在document对象上。

浏览器的重排和重绘

了解一下浏览器渲染HTML的过程
1.HTNL文件被HTML解析器解析成对应的DOM树,CSS样式文件被CSS解析器解析生成对应的样式规则表。

2.DOM树与CSS样式集解析完成后,附加在一起形成一个渲染树。
3.根据渲染树计算每个节点的几何信息。
4.渲染绘制,即根据计算完成的节点信息绘制整个页面。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值