在dom最前面插入_前端性能优化之dom编程

3e5a10a774d78593ed19b33e52566469.png

写在前面

总所周知,在前端的日常里面,采用JS操作DOM节点的情况占据了大多数。虽然现在有MVVM主流框架代替我们做了这些事情,但充分理解在DOM操作各种损耗问题,将有助于让我们更加理解和优化问题。

DOM简介

DOM,全称文档对象模型。是一个独立于语言的,用于操作XML和HTML文档的程序接口。在浏览器中,主要用来与HTML文档打交道,同样也用在Web程序上获取XML文档上,并使用DOM API用来访问文档中的数据。

但是浏览器中通常会把DOM和JavaScript独立实现。所以这两个独立的功能只要通过接口彼此连接,就会产生消耗。如果分别把DOM和JavaScript想象成孤岛,那么js每次访问DOM都需要消耗资源。访问DOM的次数越多,费用也就越高。因此,推荐的做法就是尽可能减少访问的次数。

DOM访问和操作的优化方式

避免HTML元素集合的循环操作

举个例子,编写一个函数用来修改元素文本内容。

function innerHTMLLoop(){
    for(var count = 0;count < 15000;count++){
        // 错误方式
        document.getElementById('here').innerHTML += 'a'
    }
}

该方法的问题是在每次循环时都引用innerHTML属性值,正确的做法讲需要插入的文本用一个局部变量缓存起来,再一次性插入innerHTML属性。如下:

function innerHTMLLoop2(){
    var content = ''
    for(var count = 0;i < 15000;count++){
        content += 'a'
    }
    document.getElementById('here').innerHTML = content
}

节点克隆比单纯使用创建createElement稍快

使用DOM方法更新内容的方法除了createElement之外,还可以辅助使用element.cloneNode方法替代掉createElement。

比如,我想要动态拼凑成一个表格,常规的方法肯定是在每次循环里面依次使用createElement创建节点之后,再把自己的子元素append起来。使用cloneNode之后其实大方向上不变,就是先初始化创建好各个节点,然后在循环体里替换掉createElement。

function tableCloneDOM(){
    var i , table, thead, tbody, tr, th ,a , ul ,li,
        oth = document.createElement('th'),
        otr = document.createElement('tr'),
        oa = document.createElement('a'),
        oli = document.createElement('li') ,
        oul = document.createElement('ul');
    tbody = document.createElement('tbody');
    for(var i = 1;i <= 1000;i++){
        tr = otr.cloneNode(false)
        td = otd.cloneNode(false)
        // 循环其他部分
    }
    // 生成表格
    document.getElementById('here').append(table)
}

查询普通数组的长度比查询HTML集合的长度的时间快

在DOM编程中返回HTML集合的有哪些属性和方法呢,常见的有以下几种

  • document.getElementsByName()
  • document.getElementsByClassName()
  • document.getElementsByTagName()
  • document.images
  • document.links
  • document.forms
  • document.forms[0].elements

这些属性和方法返回的是一个类似数组的列表,但并不是真正的数组。不过给提供了一个length属性,方便操作时可用。如果需要依靠该集合遍历时可以先将集合长度缓存下来。

// 1、效率低的方式,每次都计算一遍元素的长度
function loopCollection(){
    var el = document.getElementsByTagName('div')
    for(var i = 0;i < el.length;i++){
        // 代码处理
    }
}

// 2、效率稍高的方式
function loopCollectionCache(){
    var el = document.getElementsByTagName('div'),
        len = el.length
    for(var i = 0;i < len;i++){
        // 代码处理
    }
}

// 以上在遍历较小集合时可以使用第2种
// 3、但是如果集合比较大的时候,建议将集合先拷贝到一个数组,再获取数组长度之后再进行代码处理
function toArray(coll){
    for(var i = 0,a = [],len = coll.length;i < len;i++){
        a[i] = coll[i]
    }
    return a
}

var coll = document.getElementsByTagName('div')
var arr = toArray(coll)

function loopCopiedArray(){
    for(var count = 0,len = arr.length;count<len;i++){
        // 代码处理
    }
}

如果在遍历元素较多时,第3种才是合理的选择。一般情况下选择第2种即可

使用元素节点比DOM属性方便

诸如像childNodes,firstChild和nextSibling 并不区分元素节点和其他类型节点,比如注释和文本节点。在某种情况下如果我们只需要访问元素节点(可以理解为HTML标签),那么我们还需要手动去过滤掉非元素节点。这些类型检查和过滤都是不必要的dom操作。

所以,如果只想访问元素节点,那么直接使用元素节点会比操作DOM属性来的省心。

ec191f04d00892f76431f1e13079d257.png
HTML元素替换DOM属性

使用children替代childNodes会更快,因为集合项更少。HTML源码中的空白实际上是文本节点,而且它并不包含在children集合中。在所有浏览器中,children都比childNodes要快,尽管不会快太多。

选择器API少用dom链式调用的方式

使用选择器选中元素通常会有两种方式:

  1. domAPI调用,用的最多的应该是document.getElementById和document.getElementsByClassName的方式。
  2. css选择器调用,也就是document.querySelectorAll("selector")

从性能角度来讲,建议采用第2种,第一种方式的缺点很明显,从上文看,无论是getElementById还是getElementsByClassName返回的都是HTML集合,这是个比较“昂贵”的成本。尤其我们一般还不是一次就可以选中,还需要用上链式调用。

// e.g
var el = document.getElementsByTagName('div')
                 .getElementsByClassName('byby')
                 .getElementById('tag')

那么以上的调用方式,每操作一次成本翻倍。

那么采用css选择器的方式的优点就显而易见了。

// e.g
var el = document.querySelectorAll('div .byby #tag') // 采用css选择一次性过滤,一句代码就能解决

除了避免写出来代码的好处以外,由于querySelectorAll使用css选择器作为参数返回一个NodeList,包含这匹配节点的类数组对象。这个方法不会返回HTML集合,因此返回的节点不会对应实时的文档结构,因此大大避免由于HTML集合引起的性能问题。

最小化重排和重绘的优化方式

重排(reflow):当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素)时,浏览器需要重新计算元素的集合属性(其他元素的集合属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程称为重排。

重绘(repaint):当我们对DOM的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的集合属性、直接为该元素绘制新的样式(跳过了重排环节)。这个过程称为重绘。

综上可知,重绘不一定导致重排,重排一定会导致重绘。两者都会一定程度地影响性能,因此从开发角度来看应当尽可能地减少重排和重绘次数。

重排何时发生

  1. 添加或删除可见的DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(包括:外边距、内边距、边框厚度、宽度、高度等属性改变)
  4. 内容改变,例如:文本改变或图片被另一个不同尺寸的图片替代
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变

除了1、2(对于增减,移动节点)之外,其他的都是相当“昂贵”的操作。因为其他都改变了DOM的几何属性。

获取“即时”型几何属性应当少用

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()(currentStyle in IE)

由于每次重排都会产生计算消耗,大多数浏览器通过队列花修改并批量执行来优化重排过程。而以上方法强制的都是获取“最新”属性,因此浏览器不得不执行渲染队列中的“待处理变化”强制触发重排返回正确的值。

如何尽可能规避重排和重绘

缓存布局信息

考虑一个例子,将一个元素myElement元素沿对角线移动,每次移动一个像素,从100px * 100px 开始,再到500px * 500px 的位置结束。在timeout循环体可以用以下方法:

// 低效的
myElement.style.left = 1 + myElement.offsetLeft + 'px'
myElement.style.top = 1 + myElement.offsetTop + 'px'
if(myElement.offsetLeft >=500){
    stopAnimation()
}

在每次调用时都需要引用offsetLeft,每次都会触发重排,正确的做法是用一个全局变量缓存好offsetLeft的位置。

var current = myElement.offsetLeft

// 循环体内
current++
myElement.style.left = current + 'px'
myElement.style.top = current + 'px'
if(current >=500){
    stopAnimation()
}

避免逐条改变样式,使用类名去合并样式

比如有以下这段代码:

const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

优化成一个有 class 加持的样子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    .basic_style {
      width: 100px;
      height: 200px;
      border: 10px solid red;
      color: red;
    }
  </style>
</head>
<body>
  <div id="container"></div>
  <script>
  const container = document.getElementById('container')
  container.classList.add('basic_style')
  </script>
</body>
</html>

离线操作DOM

当你需要对DOM元素进行一系列操作时,可以通过将DOM脱离文档流的方式来减少重排和重绘的次数

有三种基本方法可以使DOM脱离文档:

  1. 隐藏元素,应用修改,重新显示
  2. 使用文档片段(document fragment)在当前dom之外构建一个子树,再把它拷贝回文档。(推荐
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。

一个例子,用一个函数替换列表文本信息如下:

<ul id="myList">
    <li><a href="http://phpied.com">Saure</li>
    <li><a href="http://javascript.com">Xavier</li>
</ul>

<!-- 数据-->
<script>
    var data = [
        {
            "name":"steph",
            "url":"http://nczonline.net"
        },
        {
            "name":"ross",
            "url":"http://tech.net"
        },
    ]
    // 更新数据函数
    function appendDataToElement(appendToElement,data){
        var a , li ;
        for(var i = 0,max = data.length;i<max;i++){
            a = document.createElement('a')
            a.href = data[i].url
            a.appendChild(document.createTextNode(data[i].name))
            li = document.createElement('li')
            li.appendChild(a)
            appendToElement.appendChild(li)
        }
    }
</script>
// 插入操作
var ul = document.getElementById('myList')
appendDataToElement(ul,data)

问题是每一次新条目被附加到当前DOM树都会导致重排。

下面是优化方案:

// 1.隐藏元素。方式是改变display属性,临时从文档中移除<ul>元素,然后再恢复它:
var ul = document.getElementById('myList')
ul.style.display = 'none'
appendDataToElement(ul,data)
ul.style.display = 'block'


// 2.创建文档片段,只访问一次实时的DOM,(推荐)
var frag = document.createDocumentFragment();
appendDataToElement(frag,data);
document.getElementById('myList').appendChild(frag)


// 3、为需要修改的节点创建一个备份,对副本进行操作,一旦完成,就用新节点替换旧节点
var old = document.getElementById('myList')
var clone = old.cloneNode(true) // true 代表也把子节点都复制进去
appendDataToElement(clone,data)
old.parentNode.replaceChild(clone,old)

其他方式

1.对经常需要展开/折叠这种类型的动画元素,将其使用绝对定位方式,使其脱离文档流。

2.在IE浏览器上避免在大量元素上使用:hover这个伪类。

3.千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。

采用事件委托减少事件绑定次数

当页面存在大量元素时,多次绑定事件处理器也是会影响性能的。每绑定一个事件处理器都是有代价的,或者需要更加多的js代码,或者增加了运行期的执行时间。因为事件绑定占用了处理时间,而且浏览器需要跟踪每个事件处理器,这也会占用更多的内存。

这时事件委托就是一种优雅的处理DOM事件的技术。它是基于事件会被逐层冒泡并被父级元素捕获的原理。因此,只需要给外层绑定一个事件处理器,就可以处理其子元素上触发的所有事件。

这个技术最常用的是在列表元素需要被绑定事件的时候。

// 比如一个父元素#menu需要监听子元素
document.getElementById("menu").onClick = function(evt){
    // 浏览器target
    var target = e.target || e.srcElement

    // 一些跨浏览器之间的兼容

    // 判断事件源是否为预期
    if(target.nodeName !== 'A'){
        return ;
    }
    // 浏览器阻止默认行为并取消冒泡
    if(typeof e.preventDefault === 'function'){
        e.preventDefault() // 阻止默认行为
        e.stopPropgation() // 阻止冒泡
    }else{
        e.returnValue = false
        e.cancelBubble = true
    }
}

引用

掘金小册​juejin.im
  • 《高性能JavaScript 第三章》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值