初探DOM编程

103 阅读6分钟

1、网页其实是一棵树

2、JS 如何操作这棵树

  • 浏览器往window上加一个 document 即可
  • JS用 document 操作网页
  • 这就是 Document Object Model 文档对象模型
  • 记住一个事实:DOM 很难用

3、使用 JS 操作这棵 DOM 树

1、获取元素,也叫标签

  • 有很多API:
    • window.idxxx 或者直接 idxxx
    • document.getElementById('idxxx')
    • document.getElementsByTagName('div')[0]
    • document.getElementsByClassName('div')[0]
    • document.querySelector('#idxxx')
    • document.querySelectorAll('.red')[0]
  • 这么多,用哪一个呢?
    • 工作中用 querySelectorquerySelectorAll
    • 兼容IE才用 getElement(s)ByXXX()

2、获取特定元素

  • 获取 html 元素

    • document.documentElement
  • 获取 head 元素

    • document.head
  • 获取body元素

    • document.body
  • 获取窗口(窗口不是元素)

    • window
  • 获取所有元素

    • document.all
    • 这个 document.all 是个奇葩,只有在 ie 浏览器内为 true ,是第6个 falsy 值

3、获取到的元素是个啥?

  • 显然是一个对象,需要搞清楚它的原型

  • 用 console.dir(div1) 查看原型链

    • 第一层原型:HTMLDivElement.prototype(所有 div 共有的属性)
    • 第二层原型:HTMLElement.prototype(所有HTML标签共有的属性)
    • 第三层原型:Element.prototype (包括所有XML、HTML标签的共有属性)
    • 第四层原型:Node.prototype (节点共有的属性,包括 XML 标签文本注释,HTML 标签文本注释)
    • 第五层原型:EventTarget.prototype (其中最重要的函数属性是 addEventListener() )
    • 第六层原型:Object.prototype
  • 节点 Node 包括以下几种:

    • MDN有完整描述,x.nodeType 得到一个数字
    • 数字 1 表示元素 Element ,也叫标签 Tag
    • 3 表示文本 Text
    • 8 表示注释 Comment
    • 9 表示文档 Document

4、节点的增删改查

1、增

  • 创建一个标签节点

    • let div1 = document.createElement('div')
    • document.createElement('style')
    • document.createElement('script')
    • document.createElement('li')
  • 创建一个文本节点

    • text1 = document.createTextNode('你好')
  • 标签里插入文本

    • div1.appendChild(text1)
    • div1.innerText = '你好' 或者 div1.textContent = '你好'
    • 但是不能用div1.appendChild('你好')
    • 示例:
  • 插入页面中

    • 创建的标签默认处于JS线程中
    • 必须把它插入到 head 或者body 里面,它才会生效
    • document.body.appendChild(div1)
    • 或者
    • 已经存在于页面内的元素.appendChild(div1)
  • 关于 appendChild() 方法

    • 代码:
    • 页面中有 div#test1 和 div#test2
    let div = document.createElement('div')
    test1.appendChild(div)
    test2.appendChild(div)
    
    • 请问最终 div 出现在哪里?test2 中
    • 一个元素不能出现在两个地方,除非复制一份
  • 怎么复制/克隆呢?

    • let div2 = div1.cloneNode(deep)
    • 其中的 deep 为可选元素,表示是否采用深度克隆
    • 如果为 true ,则该节点的所有后代节点也都会被克隆
    • 如果为 false ,则只克隆该节点本身

2、删

  • 两种方法

    • 旧方法: div1.parentNode.removeChild(div1)
    • 新方法:div1.remove()存在兼容问题,不兼容IE
    • 这两种方法删除 div1 后,div1 只是在树中被删除,还是存在于 JS 的内存中
  • 要彻底删除一个 node ?

div1.remover() 
div1 = null

3、改

  • 写标准属性

    • 改class: div.className = 'red' (但是会覆盖之前设置的 className
    div1.className = 'red'        //<div class='red'> </div>
    div1.className = 'blue'       //<div class='blue'> </div>
    div1.className += ' red'      //<div class='blue red'></div>
    div1.classList.add('green')   //<div class='blue red green'></div>
    
    • 改 style: div1.style = 'width:100px'(会覆盖之前的所有 style 属性
    • 改 style 的一部分:div1.style.width = '100px'
    • 注意大小写,如:div1.style.backgroundColor = 'white'
    • 改 data-* 属性: div.dataset.* = 'wbh'
  • 读标准属性

    • div.classList / a.href
    • div.getAttribute('class')/ a.getAttribute('href')
    • 两种方法都可以,第二种更加保险,但值可能会有些不同
    div1.classList                  //["blue", "red", "greenfk", value: "blue red greenfk"]
    a.href                          //若路径为相对路径,该方法会自动用网址补全该路径
    div.getAttribute('class')       //"blue red greenfk"
    a.getAttribute('href')          //为标签内 href 的原值
    

  • 改事件处理函数

    • div1.onclick 默认为null
    • 默认点击 div1 不会有任何事发生
    • 但是如果把 div1.onclick 改为一个函数 fn
    • 那么点击 div1 的时候,浏览器就会调用这个函数
    • 并且是这样调用的 fn.call(div1,event)
    • div 会被当作 this
    • event 则包含了点击事件的所有信息,如坐标
    test.onclick = function(x){
        console.log(this)
        console.log(x)
    }
    
    //test.onclick.call(this,event),其中this指的就是test,event也就是x,第一个参数
    
  • 改内容

    • 改文本内容
    div.innerText = 'xxx'
    div.textContent = 'xxx'
    //两者几乎没有区别
    
    • 改 HTML 内容
    div.innerHTML = '<strong>重要内容</strong>'
    
    • 改标签
    div.innderHTML = ''  //先清空
    div.appendChild(div2)  //再加内容
    
    • 改爸爸,想要找一个新爸爸?
    newParent.appendChild(div)  //直接这样就可以了,直接从原来的地方消失
    

4、查

  • 查爸爸
    • node.parentNode 或者 node.parentElement

  • 查爷爷

    • node.parentNode.parentNode
  • 查子代

    • node.childNodes 或者 node.children
    • 注意,node.childNodes 会获取到标签中的空格
    • 示例:
    <ul id="test">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    
    console.log(test.childNodes.length)  // 9 
    
    <ul id="test"><li>1</li><li>2</li><li>3</li><li>4</li></ul>
    
    console.log(test.childNodes.length)  // 4
    
    <ul id="test">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    
    console.log(test.children.length)  // 4
    
  • 查兄弟姐妹

    • node.parentElement.children 然后排除自己
    let all = div1.parentElement.children
    let siblings = []
    for(let i=0;i<all.length;i++){
        if(all[i] != div1){
            siblings.push(all[i])
        }
    }
    
  • 查看老大

    • node.firstChild 或者 node.children[0]
  • 查看老幺

    • node.lastChild
  • 查看上一个哥哥/姐姐

    • node.previousSibling
    • 如果要避开文本节点,node.previousElementSibling
  • 遍历一个 div 里面的所有元素

travel = (node,fn) =>{
    fn(node)
    if(node.children){
        for(let i=0;i<node.children.length;i++){
            travel(node.children[i],fn)
        }
    }
}

travel(div1,(node) => console.log(node))

5、DOM操作是跨线程的

跨线程操作

  • 各线程各司其职
    • JS 引擎不能操作页面,只能操作JS
    • 渲染引擎不能操作 JS ,只能操作页面
    • 那么 document.body.appendChild(div1)
    • 这句 JS 是如何改变页面的呢?
  • 跨线程通信
    • 当浏览器发现 JS 在 body 里面加了一个 div1 对象
    • 浏览器就会通知渲染引擎在页面里也新增一个 div 对象
    • 新增的 div 元素所有属性都照抄 div1 对象

插入新标签的完整过程

  • 在 div1 放入页面之前
    • 对 div1 的所有操作都属于 JS 线程内的操作
  • 把 div1 放入页面之时
    • 浏览器会发现 JS 的意图
    • 就会通知 渲染线程在页面中渲染 div1 对应的元素
  • 把 div1 放入页面之后
    • 对 div1 的操作都有可能触发重新渲染
    • div1.id = 'newId' 可能会触发重新渲染,如id绑定了样式
    • div1.title = 'new' 可能会触发重新渲染
    • 如果对 div1 进行多次操作,浏览器可能会合并成一次操作,也可能不会
      • 示例:
      <!DOCTYPE html>
      <html>
      <head>
          <meta charset="utf-8">
          <title>JS Bin</title>
          <style>
              .start{
                  border:1px solid red;
                  width:10px;
                  height:100px;
                  transition: width 1s;
              }
              .end{
                  width:200px;
              }
          </style>
      </head>
      <body>
          <div id="test"></div>
          <script>
              test.classList.add('start')
              test.clientWidth  //获取宽度,这句话看似无用,实则会触发重新渲染,如果没有这句话,渲染会直接合并上下两个操作
              test.classList.add('end')
          </script>
      </body>
      </html>
      

属性同步

  • 标准属性
    • 对 div1 的标准属性的修改,会被浏览器同步到页面中
    • 比如 id、className、title等
  • data-* 属性
    • 同上
  • 非标准属性
    • 对非标准属性的修改,只会停留在 JS线程中
    • 不会同步到页面里
    • 比如 <div x="test"></div> 的 x 属性
  • 启示:
    • 如果需要自定义属性,又想要把它同步到页面中
    • 请使用 data- 作为前缀