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.渲染绘制,即根据计算完成的节点信息绘制整个页面。