前言
有一家农户,圈养了几头猪。一天主人忘记关圈门,便给了几头猪逃跑的机会。经过几代以后,这些猪变得越来越凶悍以至开始威胁经过那里的行人。几位经验丰富的猎人闻听此事,很想为民除害捕获它们。但是,这些猪却很狡猾,从不上当。
「 当猪开始独立的时候,都会变得强悍和聪明了。」
有一天,来了一个老人,说来帮忙抓野猪,众乡民一听就嘲笑他:“别逗了,连好猎人都做不到的事你怎么可能做到。”但是,两个月以后,老人回来告诉那个村子的村民,野猪已被他关在山顶上的围栏里了。 村民们很惊讶,追问那个老人是怎么抓到的。老人解释说:“首先,就是去找野猪经常出来吃东西的地方。然后我就在空地中间放一些粮食作陷阱的诱饵。那些猪起初吓了一跳,最后还是好奇地跑过来,闻粮食的味道。很快一头老野猪吃了了第一口,其他野猪也跟着吃起来。这时我知道,我肯定能抓到它们了。”
当野猪要靠人类供给食物时,它就会失去生存的机智和强悍身体,接着它就被驯化了。其实人类也一样,如果你想使一个人残废,只要给他一对拐杖再等上几个月就能达到目的;换句话说,如果在一定时间内你给一个人免费的午餐,他就会养成不劳而获的习惯。别忘了,每个人在天生就有被“照顾”的需求了。
今天要聊的DOM,有几个方向:节点、属性、事件。
Dom节点
我们常说的Dom树就是Node节点树。每个节点都会有三个至关重要的属性:nodeType、nodeName、nodeValue。
而节点类型说的就是nodeType。一共有12种节点类型,常用的有三种:
元素、属性、文本
。
<div id="btn">hellodiv>
<script>
//元素节点
let btn = document.getElementById("btn");
console.log(btn.nodeType, btn.nodeName, btn.nodeValue)
//1 "DIV" null
//属性节点
let id = btn.attributes.id;
console.log(id.nodeType, id.nodeName, id.nodeValue)
//2 "id" "btn"
//文本节点
let text = btn.firstChild;
console.log(text.nodeType, text.nodeName, text.nodeValue)
//3 "#text" "hello"
script>
每个节点
之间的关系也是非常清晰的,每个节点都有一个childNodes属性,保存着一个NodeList对象,它是基于DOM结构动态执行查询的结果,因此 DOM 结构的变化能够自动反映在NodeList 对象中。
同样每个节点还有一个children的属性,它与childNodes的区别就是,
childNodes存储的是NodeList对象,而children返回的是HTMLCollection对象
。NodeList存储的不只是元素节点,也有文本节点、注释节点等等,而HTMLCollection存储的只有元素节点。如下
<div id="btn">
hello
<span>llllspan>
<p>jjjshdhrp>
div>
<script>
let btn = document.getElementById("btn");
console.log(btn.childNodes, btn.children)
//NodeList(5) [text, span, text, p, text]
//HTMLCollection(2) [span, p]
script>
无论是NodeList还是HTMLCollection都不是数组,如果我们需要转化为数组,我们可以使用扩展运算符来操作:
<ul id="tab">
<li class="active">新闻li>
<li>事件li>
<li>国际li>
ul>hdhrp>
<script>
let tab = document.getElementById("tab");
[...tab.children].map(item => {
item.addEventListener("click", function () {
this.classList.toggle("active")
})
})
script>
关于节点的计算,是
因为我们常常会忽略掉换行和空格,无论换行还是空格都是欺骗眼睛的操作,在节点中都属于文本节点
。如果遇到节点计算,不要忽视换行和空格。
如上图ul下有几个节点?
关于节点还有四个常用的方法,这些「
增
删改插
」都是针对NodeList:
<div id="btn">
hello
<span class="active">llllspan>
<p id="pnode">jjjshdhrp>
<a id="anode">a>
div>
<script>
let btn = document.getElementById("btn");
let pnode = document.getElementById("pnode");
let anode = document.getElementById("anode");
let span = document.querySelector("span");
//创建一个文本节点
let text = document.createTextNode("world")
btn.appendChild(text);//追加
btn.insertBefore(text, pnode);//插入
btn.replaceChild(text, span);//替换
btn.removeChild(anode);//删除
script>
通过Dom来修改class类名的方式常用的有以下几种:
• 通过classList
• 通过setAttribute
//获取类名
Element.classList
// //获取类名长度
Element.classList.length
Element.classList.add("active", "show")
Element.classList.remove("active", "hide")
Element.classList.toggle("active")
//判断是否包含class 返回布尔值
Element.classList.contains("active");
//通过属性添加类名
Element.getAttributeNames();//获取属性名
Element.getAttribute("class");//获取class的属性值
Element.attributes.item(0);//获取元素第一个属性键值对
Element.hasAttribute("class");//判断是否有class属性
Element.setAttribute("class", "active");//设置class属性
//移除
if (Element.hasAttribute("class")) {
Element.attributes.removeNamedItem("class")
}
自定义属性
我们常会遇到在标签上设置自定义属性来保存数据。常用的方式有以下几种:
• 直接在标签上添加
• 通过H5的data-xx来设置
• 通过attribute来设置
• 在dom对象上直接添加
<div id="btn" class="active" data-item="name" index="3">div>
<script>
let btn = document.getElementById("btn");
//自定义属性
btn.index = 1
//无法通过attribute获取,只能通过js对象获取
//通过attribute
btn.setAttribute("item", "hello")
//直接在标签上添加
btn.getAttribute("index");
//3
//通过H5的data-XX来设置
btn.getAttribute("data-item");
//name 前提你知道自定义属性是什么
//如果不知道,可以通过dataset获取
btn.dataset
//DOMStringMap {item: "name"}
btn.dataset.item
//name
script>
事件
看到事件首先想到的是事件流。
接下来看这个:
<ul id="btn">
<li class="active">新闻li>
<li>事件li>
<li id="cl">
<div id="son" onclick="ehandle(this)">
国际
div>
li>
ul>
观察下面输出顺序。
let btn = document.getElementById("btn");
let cl = document.getElementById("cl");
let son = document.getElementById("son");
function ehandle(e) {
console.log(e, "8")
}
btn.addEventListener("click", function (e) {
console.log(e, "2")
})
btn.onclick = function (e) {
console.log(e, "1")
}
btn.addEventListener("click", function (e) {
console.log(e, "3")
})
document.addEventListener("click", function (e) {
console.log(e.eventPhase);
console.log(e, "7")
})
son.addEventListener("click", function (e) {
console.log(e, "6")
})
son.onclick = function (e) {
// e.stopImmediatePropagation()
//不会执行6
// e.stopPropagation()
//会执行6
// e.preventDefault()
console.log(e.eventPhase);
//2 正常事件派发
console.log(e, "5")
}
cl.addEventListener("click", function (e) {
console.log(e, "4")
// console.log(e.bubbles) //事件是否是冒泡事件类型。
// console.log(e.cancelable) //件是否可拥可取消的默认动作。
//如果为false,表示没有默认动作
console.log(e.eventPhase);//事件传播的当前阶段。
//3 冒泡阶段
// e.stopImmediatePropagation()
// e.preventDefault()
//要执行与事件关联的默认动作
// e.stopPropagation()
//终止事件在传播过程的捕获、目标处理或起泡阶段进一步传播,
//事件不再被分派到其他节点。
})
上述的执行结果是
5=>6=>4=>2=>1=>3=>7。
先讲8为什么不执行?
因为标签上的onclick事件会被js中的onclick事件覆盖。如上题,去掉son.onclick方法,8就会被立即执行。执行结果就会变为
8=
>6=>4=>2=>1=>3=>7。
currentTarget和target的区别?
最好的例子就是在ul标签注册点击方法,点击li元素。e.currentTarget是指绑定点击事件的元素;e.target是指事件触发的元素。
stopImmediatePropagation与stopPropagation的区别?
除了都能防止事件被派发到其他节点之外,stopImmediatePropagation还能防止绑定在自身的其他事件执行,但此功能只能在onclick事件中生效。也就是说,当在onclick中执行此方法之后,关于此元素在事件处理程序中的所有事件都不会执行。
onc
lick与addEventListener("click")的优先级?
当绑定到自身,onclick会先于事件监听。当处在冒泡阶段,绑定在父元素的事件是没有优先级的。
接下来该到自定义事件了。常见的自定义事件有三种方式:
- Event
- CustomEvent
- document.createEvent
// createEvent创建事件
let new3Event = document.createEvent('Event');
// 定义事件名为'build'.
new3Event.initEvent('build', true, true);
//event.initEvent(eventType,canBubble,cancelable)
//eventType:事件名称
//canBubble:是否支持冒泡
//cancelable:是否可以用 preventDefault() 方法取消事件。
new3Event.name = "document.createEvent创建的自定义事件"
// 监听事件
document.addEventListener('build', function (e) {
// e.target matches elem
alert()
}, false);
// 触发对象可以是任何元素或其他事件目标
document.dispatchEvent(new3Event);
//CustomEvent, 可以传递跟事件关联的相关值(detail)
//可以在Web Workers中使用(与主线程分离的另一个线程)
let new2Event = new CustomEvent("build", {
bubbles: true, cancelable: true, composed: true,
detail: {
name: "传递参数"
}
});
//bubbles:是否支持冒泡事件类型。
//cancelable:是否可拥可取消的默认动作。
//composed:是否会触发shadow DOM(阴影DOM)根节点之外的事件监听器
new2Event.name = "CustomEvent事件";
document.addEventListener("build", function () {
alert(new2Event.name + new2Event.detail.name);
}, false)
/* 触发自定义事件 */
document.dispatchEvent(new2Event);
//创建一个事件对象,名字为newEvent,类型为build * /
let newEvent = new Event('build', {
bubbles: true,
cancelable: true,
composed: true
});
/* 给这个事件对象创建一个属性并赋值 */
newEvent.name = "新的事件!";
/* 将自定义事件绑定在document对象上,这里绑定的事件要和我们创建的事件类型相同,不然无法触发 */
document.addEventListener("build", function () {
alert("你触发了自定义事件!" + newEvent.name);
}, false)
/* 触发自定义事件 */
document.dispatchEvent(newEvent);
事件处理程序是异步操作,它不会影响js线程。在面试中或工作中你可能会遇到,在循环中处理事件。一般的解决方案有两种,一种是形成闭包,一种是添加自定义属性。
形成此问题的主要原因是变量声明,当
ES6出现了块级作用域,此问题也就不会存在了
。也就是说要用let,不要用var。
//会出现
for (var index = 0; index < liNodeList.length; index++) {
const element = liNodeList[index];
element.onclick = function (e) {
console.log(e, index)
}
}
//不会出现
for (let index = 0; index < liNodeList.length; index++) {
const element = liNodeList[index];
element.onclick = function (e) {
console.log(e, index)
}
}
[...liNodeList].forEach((e, i) => {
e.onclick = function (e) {
console.log(e, i)
}
})