记几个 DOM 操作技巧

使用 attributes 属性遍历元素特性

// 迭代元素的每一个特性,将它们构造成 name = value 的字符串形式
function outputAttributes (element) {
  const pairs = []
  let attrName
  let attrValue

  for (let i = 0, len = element.attributes.length; i < len; i++) {
    attrName = element.attributes[i].nodeName
    attrValue = element.attributes[i].nodeValue
    pairs.push(`${attrName}=${attrValue}`)
  }
  return pairs.join(" ")
}

使用 classList 属性来操作类名

<div class="bd user disabled">...</div>

这个 <div> 元素一共有三个类名。要从中删除一个类名,需要把这三个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串。请看下面的例子:

// div.className = 'bd user disabled'
let classNames = div.className.split(/\s+/)
let pos = -1
for (let i = 0, len = classNames.length; i < len; i++) {
  if (classNames[i] === 'user') {
    pos = i
    break
  }
}

classNames.splice(pos, 1) // 删除类名
div.className = classNames.join(' ') // 把剩下的类名拼接成字符串并重新设置

HTML5 新增了一种操作类名的方式,可以让操作更简单也更安全,那就是为所有元素添加 classList 属性,这个新类型定义了如下方法:

  • add(value):将给定的字符串值添加到列表中。如果值已经存在,就不添加了。
  • contains(value):表示列表中是否存在给定的值,如果存在则返回 true,否则返回 false。
  • remove(value):从列表中删除给定的字符串。
  • toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。

这样,前面那么多行代码用下面这一行代码就可以代替了

div.classList.remove("user")

使用 Element Traversal API 操作元素节点

过去,要遍历某元素的所有子元素,需要像下面这样写代码:

let child = element.firstChild
while (child !== element.lastChild) {
  if (child.nodeType === 1) { // 检查是否为元素节点
    processChild(child)
  }
  child = child.nextSibling
}

Element Traversal API 为 DOM 元素添加了以下 5 个属性:

  • childElementCount:返回子元素(不包括文本节点和注释)的个数。
  • firstElementChild:指向第一个子元素;firstChild 的元素版。
  • lastElementChild:指向最后一个子元素;lastChild 的元素版。
  • previousElementSibling:指向前一个同辈元素;previousSibling 的元素版。
  • nextElementSibling:指向后一个同辈元素;nextSibling 的元素版。

使用 Element Traversal 新增的 API,代码会更简洁:

let child = element.firstElementChild
while (child !== element.lastElementChild) {
  processChild(child) // 肯定是元素节点
  child = child.nextElementSibling // 遍历下一个元素节点
}

getElementsByTagName('*') 会返回什么?

最近看到一道面试题:找出页面出现最多的标签,或者说出现次数最多的前 2、3 个标签,可以试着自己实现下。

// 获取元素列表,以键值对的形式存储为一个对象
function getElements () {
  // 如果把特殊字符串 "*" 传递给 getElementsByTagName() 方法
  // 它将返回文档中所有元素的列表,元素排列的顺序就是它们在文档中的顺序。
  // 返回一个 HTMLCollection - 类数组对象
  const nodes = document.getElementsByTagName('*')
  const tagsMap = {}
  for (let i = 0, len = nodes.length; i < len; i++) {
    let tagName = nodes[i].tagName
    if (!tagsMap[tagName]) {
      tagsMap[tagName] = 1
    } else {
      tagsMap[tagName] ++
    }
  }
  return tagsMap
}

// n 为要选取的标签个数 - 即出现次数前 n 的标签名
// 将上面的方法获取的对象的键值对取出组成数组,按出现次数排序
function sortElements (obj, n) {
  const arr = []
  const res = []
  for (let key of Object.keys(obj)) {
    arr.push({ tagName: key, count: obj[key] })
  }

  // 冒泡
  for (let i = arr.length - 1; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j].count < arr[j + 1].count) { // 升序
        swap(arr, j, j + 1)
      }
    }
  }

  for (let i = 0; i < n; i++) {
    res.push(arr[i].tagName)
  }
  return res
}

function swap (arr, index1, index2) {
  let temp = arr[index1]
  arr[index1] = arr[index2]
  arr[index2] = temp
}

let res = sortElements(getElements(), 2)
console.log(res)

动态添加脚本与样式

// 动态添加脚本
function loadScript (url) {
  var script = document.createElement("script")
  script.type = "text/javascript"
  script.src = url
  document.body.appendChild(script)
} 

// 动态添加样式
function loadStyles (url) {
  var link = document.createElement("link")
  link.rel = "stylesheet"
  link.type = "text/css"
  link.href = url
  var head = document.getElementsByTagName("head")[0]
  head.appendChild(link)
} 

使用 contains() 方法判断某个节点是否为另一个节点的后代

contains() 方法用于判断某个节点是否为另一个节点的后代,调用 contains() 方法的应该是祖先节点,也就是搜索开始的节点,这个方法接收一个参数,即需要检测的节点。

console.log(document.documentElement.contains(document.body)) // true

这个例子检测了 <body> 元素是不是 <html> 元素的后代

Element.getBoundingClientRect() 及 dataset 的使用

这是 小册 上的一个例子,使用原生 JS 实现图片懒加载,需要了解这两个知识点

1.Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。具体解释及用法参考 MDN

通过 Element.getBoundingClientRect().top
window.innerHeight(当前视窗的高度)比较就可以判断图片是否出现在可视区域。

注意这个 top 是相对于当前视窗的顶部的 top 值而不是一开始的顶部。

2.通过 Element.dataset 可以获取到为元素节点添加的 data-* 属性,我们可以通过这个属性来保存图片加载时的 url。

<!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>
        * {
            padding: 0;
            margin: 0;
        } 
        .box-image {
          display: flex;
          flex-direction: column;
          align-items: center;
        }
        img {
            display: inline-block;
            height: 300px;
            margin-bottom: 20px;
        }
    </style>
</head>

<body>
    <div class="box-image">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/6.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/8.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/aotu-10.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/aotu-15.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/05/%E7%BD%AA%E6%81%B6%E7%8E%8B%E5%86%A04.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/05/%E7%BD%AA%E6%81%B6%E7%8E%8B%E5%86%A06.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/9.jpg" alt="">
        <img src="" class="image-item" lazyload="true" data-original="http://cmk1018.cn/wp-content/uploads/2019/04/aotu-16.jpg" alt="">
    </div>
    <script>
        var viewHeight = document.documentElement.clientHeight;
        // 节流:加一个 300ms 的间隔执行
        function throttle(fn, wait) {
          let canRun = true
          return function (...args) {
            if (!canRun) return
            canRun = false 
            setTimeout(() => {
              fn.apply(this, args)
              canRun = true
            }, wait)
          }
        }
        function lazyload() {
          let imgs = document.querySelectorAll('img[data-original][lazyload]') // 获取文档中所有拥有 data-original lazyload 属性的<img>节点
          imgs.forEach(item => {
            if (item.dataset.original == '') {// HTMLElement.dataset 访问在 DOM 中的元素上设置的所有自定义数据属性(data-*)集。
              return
            }
            // 返回一个 DOMRect 对象,包含了一组用于描述边框的只读属性——left、top、right 和 bottom,
            // 单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。
            let rect = item.getBoundingClientRect()
            // 其 top 值是相对于当前视窗的顶部而言的而不是绝对的顶部,所以 top 值 < window.innerHeight 的话图片就出现在底部了就需要加载
            if (rect.bottom >= 0 && rect.top < viewHeight) {
              let img = new Image()
              img.src = item.dataset.original
              // 图片加载完成触发 load 事件
              img.onload = function () {
                item.src = img.src
              }
              // 移除属性的话就不会重复加载了
              item.removeAttribute('data-original')
              item.removeAttribute('lazyload')
            }
          })
        }
        // 先调用一次加载最初显示在视窗中的图片
        lazyload();
        let throttle_lazyload = throttle(lazyload, 300)
        document.addEventListener('scroll', throttle_lazyload)
    </script>
</body>
</html>

如何渲染几万条数据且不卡住页面?

这也是小册上的,考察了利用文档碎片 (createDocumentFragment) 分批次插入节点

<!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>
  </head>
  <body>
    <ul>
      控件
    </ul>
    <script>
      const total = 100000 // 10万条数据
      const once = 20      // 每轮插入的数据条目
      const loopCount = total / once // 渲染总次数
      let countOfRender = 0
      let ul = document.querySelector('ul')
      function add() {
        // 使用文档碎片优化性能
        const fragment = document.createDocumentFragment()
        for (let i = 0; i < once; i++) {
          const li = document.createElement('li')
          li.innerText = Math.floor(Math.random() * total)
          fragment.appendChild(li)
        }
        ul.appendChild(fragment)
        countOfRender+=1
        loop()
      }
      function loop() {
        if (countOfRender < loopCount) {
          window.requestAnimationFrame(add) // 使用 requestAnimationFrame 每隔 16ms(浏览器自己选择最佳时间)刷新一次
        }
      }
    </script>
  </body>
</html>
posted @ 2019-08-13 18:08  freedom雲  阅读(334)  评论(0编辑  收藏  举报