BOM和DOM(接前两天)

147 阅读6分钟

10、节点操作

DOM 就是我们 html 结构中一个一个的节点构成的。不光我们的标签是一个节点,我们写的文本内容也是一个节点,注释,包括空格都是节点。 节点:html文档的每一个组成部分,最小的组成单位 ;类似于平面是有很多点组成的

var ul = document.querySelector('ul')
// console.log( ul.childNodes );

DOM节点分三种:元素节点、文本节点、属性节点。元素节点就是我们获取到的标签元素;标签里面的文本就是文本节点、标签上的属性就是属性节点。

要学习的节点,特指标签节点,不学习文本节点

标签之间的关系

创建标签

语法:

    document.createElement(标签名)
// var div = document.createElement('div')
// div.innerText = '盒子'
// div.className = 'box'
// div.style.width = '200px'
// div.setAttribute('name', 'mybox')
// console.log(div);

将标签插入到页面中

语法:

父.appendChild(标签) - 将标签插入到父标签的末尾

父.insertBefore(新的标签, 旧的子标签) - 将新标签添加到父标签中已经存在的某个子标签的前面

// document.body.appendChild(div)
// document.body.insertBefore(div, ul)

获取标签

// 获取子标签 - 父.children
// console.log( ul.children );
// 获取某个标签的上一个兄弟标签 - 标签.previousElementSibling
var fourth = document.querySelector('.fourth')
// console.log(fourth);
// console.log( fourth.previousElementSibling );

获取节点

获取所有子标签节点:

父.children

获取到的是一个所有子标签组成的伪数组

获取第一个子标签节点:

父.firstElementChild
console.log( ul.firstElementChild );

获取最后一个子标签节点:

父.lastElementChild
console.log( ul.lastElementChild );

获取父标签节点:

子.parentElement
console.log( fourth.parentElement );

获取上一个兄弟标签节点:

标签.previouseElementSibling
var fourth = document.querySelector('.fourth')
// console.log(fourth);
// console.log( fourth.previousElementSibling );

获取上一个兄弟标签节点:

标签.nextElemenetSibling
var fourth = document.querySelector('.fourth')
// console.log(fourth);
 console.log ( fourth.nextElementSibling );

创建标签

document.createElement('标签名字符串')

返回创建好的标签

插入节点

给父标签追加子标签:

.appendChild(子标签对象)

将新的子标签插入到某个旧的子标签前面:

.insertBefore(新的子标签, 旧的子标签)

替换节点

使用新的子标签替换掉旧的子标签:

.removeChild(新的子标签, 旧的子标签)

删除节点

父标签将指定的子标签删除:

.removeChild(子标签)

克隆节点

将一个标签复制一份出来:

标签.cloneNode()

返回一个标签对象,这样只能复制一个空的标签,没有内容。

标签.cloneNode(true)

返回一个标签对象,这样可以将标签中的内容也复制出来。

获取标签尺寸

包含标签边框的尺寸:

标签.offsetWidth
标签.offsetHeight

不包含边框的尺寸:

标签.clientWidth
标签.clientHeight

返回纯数字。

获取元素位置

标签.offsetLeft
标签.offsetTop

获取的是相对于设置过定位的父标签的左边距和上边距离,返回纯数字。

获取边框大小

标签.clientTop
标签.clientLeft

获取到的是上边框和左边框的厚度,纯数字。

替换标签

// var b = document.createElement('b')
// b.innerText = '加粗'
// 使用b标签替换掉fourth
// 父.replaceChild(新, 旧的子标签)
// ul.replaceChild(b, fourth)

删除标签

// 父.removeChild(子)
// ul.removeChild(fourth)

复制标签

// 标签.cloneNode()
// console.log( fourth.cloneNode() );
// 如果要将标签中的内容和子标签都复制出来,就给方法添加参数true
// console.log( fourth.cloneNode(true) );

总结:

创建:document.createElement()

插入:
    父.appendChild()
    父.insertBefore()
替换:
    父.replaceChild()
删除:
    父.removeChild()
复制:
    标签.cloneNode(true)
获取:
    父.children.parentElement
    标签.previousElementSibling
    标签.nextElementSibling.firstElementChild.lastElementChild

回流和重绘

我们在做案例的时候,通常一个标签要设置很多样式。为了方便我们批量设置样式,可以封装一个批量设置样式的函数:

function setStyle(ele, styleObj) {
    for(var key in styleObj) {
        ele.style[key] = styleObj[key]
    }
}

这个函数在批量设置样式的时候,每遍历一次,设置一次样式,每次设置样式都设置在了行内,这样会造成多次回流,影响页面性能。

1、浏览器渲染过程

浏览器在渲染页面的时候,大致是以下几个步骤:

  1. 解析html生成DOM树,解析css,生成CSSOM树,将DOM树和CSSOM树结合,生成渲染树;
  2. 根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系 - 回流
  3. 根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置 - 重绘
  4. 最后将得到的节点位置信息交给浏览器的图形处理程序,让浏览器中显示页面

2、回流

回流:英文叫reflow,指的是当渲染树中的节点信息发生了大小、边距等问题,需要重新计算各节点和css具体的大小和位置。

例:在css中对一个div修饰的样式中,修饰了宽度、高度等样式,浏览器需要重新计算标签大小,这个计算的过程,就是回流的过程。

容易造成回流的操作:

  • 布局流相关操作

    • 盒模型的相关操作会触发重新布局
    • 定位相关操作会触发重新布局
    • 浮动相关操作会触发重新布局
  • 节点操作

    改变节点的结构或其中的文本结构会触发重新布局。

    对标签进行下面这些属性或方法操作的时候,会强行回流:

    • offsetTop
    • offsetLeft
    • offsetWidth
    • offsetHeight
    • scrollTop
    • scrollLeft
    • scrollWidth
    • scrollHeight
    • clientTop
    • clientLeft
    • clientWidth
    • clientHeight
    • getComputedStyle
  • css

    • width
    • height
    • padding
    • border
    • margin
    • position
    • top
    • left
    • bottom
    • right
    • float
    • clear
    • text-align
    • vertical-align
    • line-height
    • font-weight
    • font-size
    • font-family
    • overflow
    • white-space

3、重绘

重绘:英文叫repaint,当节点的部分属性发生变化,但不影响布局,只需要重新计算节点在屏幕中的绝对位置并渲染的过程,就叫重绘。比如:改变元素的背景颜色、字体颜色等操作会造成重绘。

回流的过程在重绘的过程前面,所以回流一定会重绘,但重绘不一定会引起回流。

容易造成重绘操作的css:

  • color
  • border-style
  • border-radius
  • text-decoration
  • box-shadow
  • outline
  • background

4、优化

不管是回流还是重绘,都会对浏览器的渲染造成影响,所以我们在项目中,尽量避免回流。

4.1、合并样式修改

减少造成回流的次数,如果要给一个节点操作多个css属性,而每一个都会造成回流的话,尽量将多次操作合并成一个,例:

var oDiv = document.querySelector('.box');
oDiv.style.padding = '5px';
oDiv.style.border = '1px solid #000';
oDiv.style.margin = '5px';

操作div的3个css属性,分别是padding、border、margin,此时就可以考虑将多次操作合并为一次。

方法1:使用style的cssText

oDiv.style.cssText = 'padding:5px; border:1px solid #000; margin:5px;';

方法二:将这几个样式定义给一个类名,然后给标签添加类名:

<style>
    .pbm{
        padding:5px; 
        border:1px solid #000; 
        margin:5px;
    }
</style>
<script>
    var oDiv = document.querySelector('.box');
    oDiv.classList.add('pbm');
</script>

4.2、批量操作DOM

当对DOM有多次操作的时候,需要使用一些特殊处理减少触发回流,其实就是对DOM的多次操作,在脱离标准流后,对元素进行的多次操作,不会触发回流,等操作完成后,再将元素放回标准流。

例:

var data = [
    {
        id:1,
        name:"商品1",
    },
    {
        id:2,
        name:"商品1",
    },
    {
        id:3,
        name:"商品1",
    },
    {
        id:4,
        name:"商品1",
    },
    // 假设后面还有很多
];
var oUl = document.querySelector("ul");
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    oUl.appendChild(oLi);
}

这样每次给ul中新增一个li的操作,每次都会触发回流。

方法1:方法一:隐藏ul后,给ul添加节点,添加完成后再将ul显示

oUl.style.display = 'none';
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    oUl.appendChild(oLi);
}
oUl.style.display = 'block';

此时,在隐藏ul和显示ul的时候,触发了两次回流,给ul添加每个li的时候没有触发回流。

方法二:创建文档碎片,将所有li先放在文档碎片中,等都放进去以后,再将文档碎片放在ul中

var fragment = document.createDocumentFragment();
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    fragment.appendChild(oLi);
}
oUl.appendChild(fragment);

文档碎片就是一个虚拟的DOM节点。对文档碎片操作不会造成回流。

方法三:将ul拷贝一份,将所有li放在拷贝中,等都放进去以后,使用拷贝替换掉ul

var newUL = oUl.cloneNode(true);
for(var i=0;i<data.length;i++){
    var oLi = document.createElement("li");
    oLi.innerText = data[i].name;
    newUL.appendChild(oLi);
}
oUl.parentElement.replaceChild(newUl, oUl);

4.3、避免多次触发布局

如下回到顶部的操作:

goBack.onclick = function(){
    setInterval(function(){
        var t = document.documentElement.scrollTop || document.body.scrollTop;
        t += 10;
        document.documentElement.scrollTop = document.body.scrollTop = t;
    },20)
}

每隔20毫秒都会重新获取滚动过的距离,每次都会触发回流,代码优化如下:

goBack.onclick = function(){
    var t = document.documentElement.scrollTop || document.body.scrollTop;
    setInterval(function(){
        t += 10;
        document.documentElement.scrollTop = document.body.scrollTop = t;
    },20)
}

只获取一次,每次都让数字递增,避免每次都获取滚动过的距离。

对于页面中比较复杂的动画,尽量将元素设置为绝对定位,操作元素的定位属性,这样只有这一个元素会回流,如果不是定位的话,容易引起其父元素以及子元素的回流。

4.4、修改批量设置样式函数

ul添加10li
// for (var a = 0; a < 10; a++) {
//     var li = document.createElement('li')
//     li.innerText = a + 1
//     ul.appendChild(li)
// }
// 循环10次,每循环一次,就改变ul的结构一次,造成了10次回流
// 优化
// 套路1:先复制ul,给复制出来的ul添加li,使用添加了liul替换掉原ul
// var newUL = ul.cloneNode(true)
// // console.log(newUL);
// for (var a = 0; a < 10; a++) {
//     var li = document.createElement('li')
//     li.innerText = a + 1
//     newUL.appendChild(li)
// }
// console.log(newUL);
// ul.parentElement.replaceChild(newUL, ul)

// 套路2:将标签隐藏,进行操作,再显示
// ul.style.display = 'none'
// for (var a = 0; a < 10; a++) {
//     var li = document.createElement('li')
//     li.innerText = a + 1
//     ul.appendChild(li)
// }
// ul.style.display = 'block'

// 套路3:利用文档碎片(虚拟标签)
// var fg = document.createDocumentFragment() // 创建文档碎片
// for (var a = 0; a < 10; a++) {
//     var li = document.createElement('li')
//     li.innerText = a + 1
//     fg.appendChild(li)
// }
// console.log(fg);
// // 将文档碎片放在ul中
// ul.appendChild(fg)
function setStyle(ele, styleObj) {
    var cssText = ''
    for(var key in styleObj) {
        var cssProp = key
        for(var a = 0; a < cssProp.length; a++) {
            var charCode = cssProp.charCodeAt(a)
            if(charCode >= 65 && charCode <= 90) {
                cssProp = cssProp.slice(0, a) + '-' + cssProp[a].toLowerCase() + cssProp.slice(a+1)
            }
        }
        cssText += cssProp + ':' + styleObj[key] + ';'
    }
    ele.style.cssText = cssText
}