元素操作
DOM 树里每一个内容都称之为节点,元素操作实质上是元素节点操作。节点类型又分为以下三种:
- 元素节点
- 所有的标签 比如
body、div html是根节点
- 所有的标签 比如
- 属性节点
- 所有的属性 比如
href
- 所有的属性 比如
- 文本节点
- 所有的文本
重点记住元素节点,可以更好的让我们理清标签元素之间的关系。
使用原生js实现内容操作
增
增加的本质就是在赋值
innerTextinnerHTML
改
修改的本质是重新赋值
innerTextinnerHTML
删
删除的本质是赋值空字符串
innerTextinnerHTML
查
查询的本质是获取值
innerTextinnerHTML
使用原生js实现属性操作
增
- 添加内置属性:元素.属性 = 值
- 添加自定义属性
document.setAttrute()data-
删
没学过
修
- 元素.属性 = 新值
查
- 元素.属性
使用原生js实现元素操作
查找节点
基于文档查找
-
document.querySelector(选择器):在整个文档中查找满足条件的元素,只返回第一个,返回值是一个dom元素,就可以直接操作 -
document.querySelectorAll(选择器):在整个文档中查找所有满足条件的元素,返回一个伪数组- 不管有没有找到都是返回伪数组
- 伪数组不能直接操作,需要遍历出每一个
dom元素再操作
基于元素查找
除了用基于文档查找的方式获取元素外,还可以用元素节点之间的关系来获取、查找元素。
-
父节点
-
parentNode返回最近一节父元素节点,找不到返回
null。<div> <p></p> </div>console.log(p.parentNode); // div console.log(div.parentNode); // null
-
-
子节点
<div class="erweima"> <!-- abc --> <span>aaa</span> <img src="images/code.png" alt="" /> <span> X3 </span> </div>-
childNodeslet erweima = document.querySelector('.erweima') console.log(erweima.childNodes) // NodeList(9) [text, comment, text, span, text, img, text, span, text]获得所有子节点、包括文本节点(空格、换行)、注释节点等。
-
childrenlet erweima = document.querySelector('.erweima') console.log(erweima.children) // HTMLCollection(3) [span, img, span]- 仅获得所有元素节点
- 返回的还是一个伪数组
- 该伪数组不支持使用
forEach进行遍历。
-
-
兄弟节点
-
下一个兄弟元素节点
nextElementSibling 属性 -
上一个兄弟元素节点
previousElementSibling 属性
注意:
还有方法可查询下一个或上一个兄弟节点,包含所有节点(注释,文本),不常使用,了解即可。
- 下一个兄弟元素节点
nextSibling - 上一个兄弟元素节点
previousSibling
-
增加节点
很多情况下,我们需要在页面中增加元素。一般情况下,可采取新增节点的操作:
1.创建一个新的节点
2.把创建的新的节点放入到指定的元素内
-
创建节点
即创造出一个新的网页元素,再添加到网页内,一般先创建节点,然后插入节点。
let 变量 = document.createElement('标签名') -
追加节点
要想在界面看到,还得插入到某个父元素中
-
插入到父元素的最后一个子元素:
父元素.appendChild(创建的新标签变量)会将子元素追加到父容器的最后
-
插入到父元素中某个子元素的前面:
父元素.insertBefore(创建的新标签变量, 参考的子元素)会将指定的元素插入到某个子元素之前。如果第二个参数写
null,则效果与appendChild一致,做追加。但是不能不写第二个参数,否则报错。 -
克隆节点
无论用
appendChild方法还是insertBefore都是把原来父元素的子节点挪到新的父元素下,导致原父元素的子节点被删掉。克隆则是把该子节点复制新一份,不会删除原来的子节点。元素.cloneNode(布尔值)- 若为
true,则代表克隆时会包含后代节点一起克隆 - 若为
false,则代表克隆时不包含后代节点 - 默认为
false
-
删除节点
-
通过父元素删除
父元素.removeChild(要删除的元素)- 如不存在父子关系则删除不成功,不能用爷爷删孙子,一定是要父子关系。
- 删除节点和隐藏节点(
display:none) 有区别的: 隐藏节点还是存在的,但是删除,则从html中删除节点。
-
删除自身
元素.remove()
重绘和重排
浏览器渲染页面的步骤如下所示:
- 解析(
Parser)HTML,生成DOM树(DOM Tree) - 同时解析(
Parser)CSS,生成样式规则 (Style Rules) - 根据
DOM树和样式规则,生成渲染树(Render Tree) - 进行布局
Layout(回流/重排):根据生成的渲染树,得到节点的几何信息(位置,大小) - 进行绘制
Painting(重绘): 根据计算和获取的信息进行整个页面的绘制 Display: 展示在页面上
重绘
由于节点(元素)的样式的改变并不影响它在文档流中的位置和文档布局时(比如: color 、background-color 、outline 等), 称为重绘。
即当元素节点发生颜色、背景颜色、字体大小这种不影响整体布局排列以及宽高、仅仅是视觉效果上的改变,则会引起重绘。
重排
当 Render Tree 中部分或者全部元素的尺寸、结构、布局等发生改变时,浏览器就会重新渲染部分或全部文档的过程称为 回流。
即当元素节点发生边框、内外边距、宽高等这种改变自身大小以及整体布局的属性时,会引发回流,也称为重排。
总结
重绘不一定引起回流,而回流一定会引起重绘
案例
元素操作
整体效果如下图所示。
<h1>城市选择:</h1>
<!-- multiple:选择多项 -->
<select id="src-city" name="src-city" multiple>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">深圳</option>
<option value="4">广州</option>
<option value="5">西红柿</option>
</select>
<div class="btn-box">
<!--实体字符-->
<button id="btn1">>></button>
<button id="btn2"><<</button>
<button id="btn3">></button>
<button id="btn4"><</button>
</div>
<!-- multiple 支持多选 -->
<select id="tar-city" name="tar-city" multiple></select>
点击第一个按钮时,左侧选择表单内的所有元素移动到右边;点击第二个按钮时,右边所有的元素移动到左边;点击第三个按钮时,左边被选中的元素移动到右边;点击第四个按钮时,右边被选中的元素移动到左边。
解题思路:
-
先获取事件源,左右两侧的选择表单与四个按钮。为四个按钮绑定点击事件。
let srcCity = document.querySelector('#src-city') let tarCity = document.querySelector('#tar-city') let btn1 = document.querySelector('#btn1') let btn2 = document.querySelector('#btn2') let btn3 = document.querySelector('#btn3') let btn4 = document.querySelector('#btn4') -
当第一个按钮触发点击事件后(第二个按钮操作类似,故不赘述):
-
获取左侧表单的所有孩子元素节点
children,打印出来可以看到获取到所有的option子元素。 -
对所有的
option子元素用for进行循环遍历,再appendChild追加到右侧表单中。 -
不用
forEach遍历是因为用appendChild获取到的子元素伪数组没有forEach方法,如果使用forEach则会报以下错误。
注意:
遍历完一次,要把变量
i自减一,让索引恢复。如果没有自减一,会以一下流程进行运作:初始值i为0,索引值为0的元素 “北京” 被移动到右侧表单中,此时左侧表单还剩下4个元素,索引i++,值为1,进行下一次的遍历,让索引值i为1的元素移动到右侧。由于“北京”已经被移动到右侧,因此“上海”变为索引第0项。而索引号此时查找的是1,因此会跳过“上海”把“深圳”移动到右侧。最后会造成有两个元素遗留在左侧。
这也是为什么在增删改查操作数据时是依据
id来操作而不是依据索引号,因为索引号会有变更,不是唯一对应值。- 最后再注重细节,判断如果左侧没有元素(即左侧孩子长度为0)则直接跳出循环,函数结束使用
return,后面的语句不再执行。
btn1.addEventListener('click', function() { let options = srcCity.children for (let i = 0; i < options.length;) { if (options.length == 0) { return } tarCity.appendChild(options[i]) } }) btn2.addEventListener('click', function() { let sta = tarCity.children for (let i = 0; i < sta.length;) { if (sta.length == 0) { return } srcCity.appendChild(sta[i]) } }) -
-
点击第三个按钮触发回调函数,执行以下操作(第四个按钮的操作类似,故不过多赘述):
-
获取当前表单内所有的子元素,用变量
option接收。 -
对
option进行循环遍历。 -
用
if判断,如果孩子元素处于被选择状态,则移动到右侧去。让变量i自减一,从0开始继续遍历。
注意:
这里之所以不会造成死循环,是因为我们把变量自减一写在了 i
f判断里,只有符合被选中状态才让变量自减一,如果不符合则继续执行下去。btn3.addEventListener('click', function() { let options = srcCity.children for (let i = 0; i < options.length; i++) { if (options[i].selected) { tarCity.appendChild(srcCity.children[i]) i-- } } }) btn4.addEventListener('click', function() { let sta = tarCity.children for (let i = 0; i < sta.length; i++) { if (sta[i].selected) { srcCity.appendChild(tarCity.children[i]) i-- } } }) -
实现一个简单的日期插件
自制一个时间插件,让页面可显示当前时间获取显示倒计时。
显示当前时间
function timer(document) {
setInterval(function() {
let date = new Date()
let year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
let hour = date.getHours()
let minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
let second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return document.innerHTML =
`今天是${year}年${month}月${day}日,现在是${hour<10?'0'+hour:hour}:${minute}:${second}`
}, 1000)
}
timer(document.querySelector('h1'))
本案例没有复杂的点,先 new 一个事件对象,再一一调用即可。
倒计时
先获取现在的时间戳,再获取将来的时间戳,进行相减再换算即可获取相差时间,在定时器内自减一即可。减到0就停止定时器。
由于时间戳是以毫秒为单位,因此换算公式如下:
- 小时:毫秒 / 1000 (秒) / 60 (分钟) / 60 (小时),取整数即可。
- 分钟:毫秒 / 1000 (秒) % 3600 (小时的余数即分钟) / 60 ,取整数即可。
- 秒: 毫秒 / 1000 % 60 。
// 获取将来的时间
let date = new Date('2022-1-27 18:30:00')
let oldTime = date.getTime();
// 开启定时器,让倒计时自动开启
function times(oldTime, hourEle, minuteEle, secondEle, document) {
let timeId = setInterval(() => {
// 获取现在的时间
let nowTime = Date.now();
// 相减除以1000,把毫秒转换为秒
let res = (oldTime - nowTime) / 1000
// 判断,如果时间走完则停止定时器
if (res === 0) {
clearInterval(timeId)
}
// 计算出时、分、秒
let hour = parseInt(res / 3600)
let minute = parseInt(res % 3600 / 60)
let second = parseInt(res % 60);
// 把算出来的时分秒输出到相应的模块中,不够2位数在前面补0
hourEle.innerHTML = hour < 10 ? '0' + hour : hour
minuteEle.innerHTML = minute < 10 ? '0' + minute : minute
secondEle.innerHTML = second < 10 ? '0' + second : second
timer = new Date()
let nowHour = timer.getHours()
let nowMinute = timer.getMinutes()
let nowSecond = timer.getSeconds()
document.innerHTML =
`现在是${nowHour<10?'0'+nowHour:nowHour}:${nowMinute<10?'0'+nowMinute:nowMinute}:${nowSecond<10?'0'+nowSecond:nowSecond}`
}, 1000)
}
times(oldTime, hourEle, minuteEle, secondEle, document.querySelector('.tips'))
错误汇总
-
使用
forEach遍历所有子节点let sta = tarCity.children sta.forEach(element => { console.log(1); });会报以下错误。
this.children.forEach is not a function parentNode.children获取所有元素节点,不包括文本和注释,返回的是一个伪数组,但不能使用
forEach进行遍历。可以看到,该伪数组没有
forEach方法,因此会报错。 -
删除失败
删除子节点时出现报错。
Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.执行
removeChild失败了,你要删除的节点不是当前node的子节点。如果使用爷爷删孙子之类的就会出现这种错误,一定是要父子关系才行。
-
追加失败
追加元素时出现报错。
Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.Failed to execute 'insertBefore' on 'Node': parameter 1 is not of type 'Node'.参数1不是类型
node。 错误为添加的不是节点类型。常见的犯错行为是创建元素时没加引号;let li = document.createElement(li)以及追加元素时为参数添加引号,此时该参数会被解析为字符串,而不是变量。
tarCity.appendChild('li')