浅谈DOM编程

312 阅读8分钟

什么是 DOM 编程

我们使用HTML写的网页其实是一棵树,是由 html 标签不断向下细分很多个子节点,最终形成的一棵树 而 JS 需要操作这棵树,于是浏览器在 window 上添加了一个 document,而 DOM(Document Object Model)的意思就是 JS 用 document 来操作这棵网页树,我们使用window.document就可以获取整个网页的所有元素

怎么使用 DOM (获取元素的 API)

获取方法

  1. 当一个元素拥有ID,那么就可以直接用window.XXX(这个ID)获取到这个元素

    • window是可以省略的
    • 有时ID会和全局属性冲突,这种情况下第一种方法就不能用了
    • 这种情况下document.getElementById('XXX')(ID名)可以获取到这个元素并且不冲突(平常时候这个方法没啥用还繁琐)
  2. document.getElementsByTagName('div')[0](找到页面中所有div元素,方括号表示对其中第几个进行操作。)

  3. document.getElementsByClassName('red')[0](找到页面中所有Class为red的元素,方括号意义同上)

    • 2、3方法只有需要兼容IE的时候才会使用,平常他们又长又难用,没人会用
  4. document.querySelector('#IDxxx')(找到ID为XXX的元素)

  5. document.querySelectorAll('#IDxxx')[0](找到所有满足这个条件的元素,后面的方括号表示要用里面的第几个元素)

获取特定元素

  • 获取HTML标签:document.documentElement
  • 获取head元素:document.head
  • 获取body元素:document.body
  • 获取窗口(窗口不是元素):window
  • 获取所有元素:document.all(这句话只在IE浏览器中运行为真,其他浏览器运行都为假。可以利用这一特点用if....else语句来写一些只对IE生效的代码)(但是如果不进行判断直接用这句话则一点问题都没有)
    • 这也是JS的第六个falsy值

获取到的元素是个啥?(元素的6层原型链)

首先我们抓来一只野生 div 对象来看看(console.dir(div)来查看原型链)

  1. 第一层原型 HTMLDivElement.protopyte
    • 这里面是所有 div 共有的属性,不用细看
  2. 第二层原型 HTMLElement.protopyte
    • 这里面是所有 HTML 标签的属性,不用细看
  3. 第三层原型 Element.protopyte
    • 这里面是所有 XML 、 HTML 标签的共有属性。你不会以为浏览器只会展示 HTML 吧
  4. 第四层原型 Node.protopyte
    • 这里面是所有节点共有的属性,节点包括 XML 标签文本注释、HTML 标签文本注释等等
  5. 第五层原型 EventTarget.protopyte
  6. 最后一层原型就是 Object.protopyte了

怎么创建元素的 API

节点的增

创建一个标签节点

  • let div1 = document.createElement('div')(创建一个div标签)
  • 同理,style、script、li标签都是这样创建

创建一个文本节点

  • text1 = document.createTextNode('你好')(创建一个“你好”的文本节点)
  • 为什么不直接用你好来创建呢?因为直接创建的话你好就是一个字符串,而用节点的方式创建出来你好则为节点

如何在标签里面插入文本

  • 第一种方法:div1.appendChild(text1)(要注意的是标签节点和文本节点都已经创建完成)
  • 第二种方法:div1.innerText='你好'或者div1.textContent='你好'(同理,标签节点和文本节点都要已经创建完成)
    • 两种方法是不同接口提供的不同方法,第一种方法是由Element提供的,第二种方法是由Node提供的

创建的标签、文本节点如何插入到页面中

  • 创建的标签默认处于JS线程中,必须把它插入到head或者body中才能生效
    • 插入方法:document.body.appentChild(div1)或者已在页面中的其他元素.appent.Child(div1)

appendChild需要注意的地方

  • 假如页面中有div#text1和div#text2
let div1 = document.createElement('div')
text1.appendChild(div)
text2.appendChild(div)

最终div出现在text2里面,因为text2是后写入的,而一个元素不能同时出现在两个地方,除非复制一份。所以最终出现在text2中

克隆节点

  • let div2 = div1.cloneNode(true)(深度克隆,该元素内所有子元素都会被克隆)
  • let div2 = div1.cloneNode(false) (浅克隆,只克隆该节点本身)

节点的删除

两种方法

  • 第一种:div1.parentNode.removeChild(div1)(告诉它父亲删除它,这种方法只是把节点从树里面删除掉,实际上还在内存中,随时可以加回来)
  • 第二种:div1.remove()(这种方法很简便,但是不兼容IE。这种方法也只是把节点从页面中移除,但还在内存中)
  • 如何彻底删掉节点?(首先要确定该节点已经从页面中移除)div1 = null

改属性

写标准属性

  • 改ID:div2.id = 'div2'
  • 改class:div.className = 'red'(这种方法会覆盖之前的属性)。div.className += ' red'(这样写不会覆盖,会在原有基础上后加新的属性)
    • 新语法:div.classList会查看当前div当前所有class。例如div.classList.add('red')(不覆盖原有属性的基础上加入新属性)
  • 改style:div.style = 'color:bule'(这样会覆盖之前内容)
    • 可以这样写:div.style.width = '200px'(只修改style中的width)
    • JS不支持‘-’符号,有这样符号的时候只需要删除这个符号,然后把符号后面的字母写成大写就可以了

读属性

  • div.id(大部分属性都可以这样读取,直接写出来就行)
  • div.classList(class读取和别的不太一样)

改内容

改文本内容

  • div.innerText = 'hi'(修改div里面的文本内容)
  • div.textContent = 'hi'(同上,二者几乎没有区别)

改HTML内容(标签里面加或改标签)

  • div.innerHTML = '<strong>你好</strong>(在div里加一个strong标签,要注意HTML全是大写)

改标签(删掉这个标签换成别的标签)

  • div.innerHTML = ''(先清空内容)
  • div.appendChild(div2)(加入新的标签)

改爸爸(让自己去另一个标签里面)

  • newParent.appendChild(div)

改事件处理函数

  • div.onclick可以给div添加一个函数,点击div可以调用这个函数,默认值为null,点击不会发生任何事情
  • text.onclick = function( ) {}(text是这个div的id)(onclick赋予函数可以让这个div点击后运行该函数)
    • 调用原理是fn.call(div,event)(div是该元素,event是事件的所有详细信息)

查看元素的API

查爸爸

  • div2.parentNode
  • div2.parentElement

查爷爷

  • div2.parentNode.parentNode(就是查爸爸调用两次)
  • div2.parentElement.parentElement

查儿子

  • div2.childNodes(要注意nodes有一个s,因为儿子可能有很多)
    • 要注意获取子元素的时候有时会获取到你不想要的东西,比如空格和回车也会算作子元素
  • div2.children(这种方法不会获取到空格回车之类的无聊的玩意,推荐使用)

查兄弟姐妹

  • div2.parentElement.children(先获取父元素,再获取父元素的子元素,这样自己也会包含在里面,还需要遍历一遍所有元素,把自己删出去)
    • 排除自己的方法:
    let siblings = []
    let c2 = div2.parentElement.children
    for(let i = 0;i<c2.length;i++){
     ifc2[i]!==div2{
      siblings.push(c2[i])
     }
    }
    

查看第一个儿子

  • document.body.firstChild(查看body中的第一个儿子)

查看最后一个儿子

  • document.body.lastChild(查看body中的最后一个儿子)

查看离我最近的哥哥姐姐

  • div2.previousSibling(查看div2前面最近的一个元素)要注意这个方法可以查找到文本节点,也就是说如果前面有个回车的话也会被查找出来
  • div2.previousElementSibling(避开文本节点,只寻找元素节点)

查看离我最近的弟弟妹妹

  • div2.nextSibling(查看div2后面最近的一个元素)要注意这个方法可以查找到文本节点,也就是说如果后面有个回车的话也会被查找出来
  • div2.nextElementSibling(避开文本节点,只寻找元素节点)

DOM操作跨线程

  • 浏览器分为渲染引擎和JS引擎
  • 各线程各司其职,JS引擎只能操作JS,渲染引擎只能渲染

跨线程通信

  • 当浏览器发现JS在body里面加了一个div1对象
  • 浏览器会通知渲染引擎在页面上也新增一个div元素
  • 浏览器通知引擎所消耗的时间会较多,所以跨线程通信会比较慢
  • 跨线程通信图示:

插入新标签的完整过程

  • 在div1放入页面之前
    • 你对它的所有操作都属于JS线程内的操作
  • 把div1放入页面之时
    • 浏览器会发现JS的意图
    • 浏览器会通知渲染线程在页面中渲染div1对应的元素
  • 把div1放入页面之后
    • 你对div1的操作都有可能触发重新渲染
    • 如果你连续对div1进行多次操作,浏览器可能会合并成一次操作,也可能不会,不同浏览器可能有不同逻辑
    • 如果不想合并,就需要中间插入一个强制执行渲染,再继续后面操作的一个操作

属性同步

标准属性

  • 对div1的标准属性的修改,会被浏览器同步到页面中

data-***

  • 同上

非标准属性

  • 对非标准属性的修改,只会停留在JS线程中,不会同步到页面里

启示

  • 如果你要写自定义属性,又想同步到页面中,请使用data- 作为前缀
JS线程中的属性我们一般称为properties,渲染线程中的属性我们一般称为attributes
  • JS线程中div1的所有属性,叫做div1的properties
  • 渲染线程中div1对应标签的属性,叫做attributes
区别
  • 大部分时候,同名的 properties和attributes值相等
  • 但如果不是标准属性,则只会在最开始时相等,后面修改不会同步
  • 要注意attributes只支持字符串,其他一切东西都会自动转化成字符串
  • 而properties支持的就很多了,比如数字、布尔等等