DOM编程

618 阅读9分钟

网页其实是一棵树

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>标题</title>
  </head>
  
  <body>
    <header>
      <h1>文字1</h1>
    </header>
    
    <main>
      <p>文字2
          <span>文字3</span>
      </p>
    </main>
    
    <footer>
        <div>文字4</div>
    </footer>
  </body>
  
</html>

上面的代码用树状图理解如下:

DOM(文档对象模型)

html内容由渲染引擎来渲染到页面上,而JS引擎负责的是JS内容,是操作不了文档内容的,咋办? 于是让浏览器来提供办法。 浏览器往window上加一个document对象,这时JS就可以用document来操作网页了,这就是document object model,即文档对象模型(把网页抽象成对象,然后来操作它)

获取元素

标签就是元素,元素就标签。

<body>
	<div id="test" class="red">测试</div>
    
    <script>
    	//1.通过id获取元素,记得带引号
        document.getElementById('test')
        
        //2.通过标签名,获取所有同名的标签,返回的是伪数组,所以要加下标
        document.getElementsByTagName('div')[0]
        
        //3.通过css类名,获取所有同css类名的标签,返回的是伪数组,也要加下标
        document.getElementsByClassName('red')[0]
        
        //4.document.querySelector('#id')
        document.querySelector('#test')
        
        //5.返回的是伪数组,要加下标
        document.querySelectorAll('.red')[0]
        
        /*
        补充:
        
        前三个getElement(s)...一般在兼容IE的时候会用,其他时候一般推荐用后两个querySelector(All)...
        
        querySelector(All)借鉴了CSS的语法,写起来很方便一些,而且写的复杂一些也是可以的:
        
        比如:querySelector('div>span:nth-child(2)')
        
        */
    </script>
</body>

获取特定的元素

// 获取html元素
document.documentElement

// 获取head元素
document.head

// 获取body元素
document.body

// 获取所有元素
document.all
// 这个document.all比较奇葩,是JS中的第六个falsy值

// 比较特殊的一个:获取窗口,注意窗口不是元素
window

获取到的元素是对象

获取到的元素(标签)是对象,既然是对象,就要搞清楚它的原型链是怎样的。 通过以下代码来查看div对象的原型链:

let test = document.querySelector('#test')
console.dir(test)

查看结果可知: div的自身属性有:id、className、classList、style、onclick等,__proto__属性指向HTMLDivElement;

第一层原型:HTMLDivElement.prototype,里面包含所有div共有的属性;

第二层原型:HTMLElement.prototype,里面包含所有HTML标签共有的属性,不用细看

第三层原型:Element.prototype,里面包含XML和HTML标签的共有属性,浏览器不仅仅能展示HTML内容,也能展示XML内容;

第四层原型:Node.prototype,里面包含所有节点的共有属性,节点包括:XML文本注释、HTML标签文本注释等

第五层原型:EventTarget.prototype,里面有个比较重要的函数属性:addEventListener;

第六层原型:Object.prototype,里面包含了对象的共有属性;

下面是div的原型链图示:

节点和元素

要注意区分,节点和元素并不是完全相同的东西,节点的范围要比元素大。

浏览器控制台里使用如下代码可以获取一个东西的节点类型:

x.nodeType

// 比如:
test.nodeType
// 返回1

根据返回数字的不同,可以知道不同的节点类型:

1:元素 Element ,叫标签 Tag 也可以;

3: 文本 Text;

8:注释 Comment;

9:文档 Document;

11:文档片段 DocumentFragment

节点的增删改查

  1. 创建一个标签节点
document.createElement('div');
document.createElement('style');
document.createElement('script');
document.createElement('li');
  1. 创建一个文本节点
document.createTextNode('你好')
  1. 往标签里插入文本
let div1 = document.createElement('div');
div1.appendChild(text1);
div1.innerText = '你好'; // IE
div1.textContent = '你好'; // Chrome,Firefox等

// 易错的错误示范:
div.appendChild('你好')

创建的元素插入页面中

我们创建完标签后,标签不会直接显示在页面中,而是出于JS线程里的,必须要把它插到 head , body 或其他已经在页面里的标签里才会生效;

document.head.appendChild(test)
document.body.appendChild(test)

一个元素不能同时出现在两个地方

代码:

// html:
<body>
	<div id="div1"></div>
    <div id="div2"></div>
</body>

// js:
let test = document.createElement('div');
div1.appendChild(test);
div2.appendChild(test);

// 问:test最终会出现在哪里?
// 1.出现在div1里面
// 2.出现在div2里面
// 3.同时出现在div1和div2里面

// 答:div2里面
// 一个元素不能同时出现在两个地方,除非复制一份

方法:

let div1 = document.createElement('div');
let text = document.createTextNode('我是一个子节点')
div1.appendChild(text)

// 父节点删子节点
// parentNode.removeChild(childNode)
div1.removeChild(text);

// 子节点自删
// childNode.remove()
text.remove();

思考:一个节点被移出页面了,也就是被从 DOM 树中移除了,那么它想回到页面上,需要重新创建一个相同内容吗?

答案:不需要,因为从 DOM 树中被移除后,被移除的节点依然在JS线程中,想要再次回到页面中,直接找一个元素 appendChild( )就可以了。

改属性

// 改class:这样改完后会覆盖之前的类
div.className = 'red blue';

// 改class:添加一个类
div.classList.add('red');

// 改style:改变总体样式
div.style = 'width:100px;color:blue;'

// 改style:改部分样式
div.style.width = '200px';

// 改style需要注意的大小写问题:
// 比如修改背景色,在css里是:
<style>
	div {
    	background-color:orange;
    }
</stlye>
//在JS里要这样改:
div.style.backgroundColor = 'orange';
//这样改也行,但是写起来比较麻烦
div.style['backgroundColor'] = 'orange';

// 改data-自定义属性:
// 有时我们需要在文档的标签里,用 data-* 来自定义一些属性:
<body>
	<div id="test" data-x="xxx"></div>
</body>
//改变自定义属性的值的方法有点奇葩,但是还是要记住:
<script>
	test.dataset.x = 'hhh';
    // 直接用 test.data-x是无法生效的,因为减号 - 这个符号,在JS里是不能用的,这里可以参考 css 里的 background-color
</script>

改事件处理函数

div.onclick默认值为null,也就是div被点击的时候默认不会有任何事发生,只有我们把一个函数 fn 赋值给 div.onclick 时,点击div时,浏览器才会去调用这个函数,产生相应的动作。

调用的时候,如果写成call函数形式的话是这样:fn.call(div,event),也就是说div会被当作this,浏览器会在用户点击的时候自动的调用,event则包含了点击事件的所有信息。

div.onclick = function fn(){
	console.log('you clicked the div')
}

用div.onclick时,只能对应一个函数,如果再把另外一个函数赋值给div.onclick,就会覆盖掉之前的函数。那么如果想达到点击一下完成多个动作怎么办呢?这时可以使用div.onclick的升级版:div.addEventListener来改事件处理函数:

function fn1(){
    console.log('hi')
}

function fn2(){
    console.log('你好')
}

div.addEventListener('click',fn1);
div.addEventListener('click',fn2);

// 点击div后,会同时打印 hi 和 你好 

改内容

  1. 改标签里的文本内容:
div.innerText = 'hi'

div.textContent = 'hi'

// 这两种方法都可以改变标签里的文本内容,两者几乎没有什么区别
// 以前 innerText 是用在IE里的,而 textContent 是用在Chrome、Firefox等其他浏览器中的
// 如今几乎所有的浏览器都同时支持这两种方法
  1. 改标签里的HTML内容: 如果不仅仅想在div标签里加文本内容,还要加一个span标签,这时就需要用到innerHTML了:
div.innerHTML = '<span>这里有个span标签</span>'

改父节点

假如span的父节点是div1,想把span放到另外一个div2节点下面,也就是改变它的父节点:

div2.appendChild('span')

// 直接这样写就可以了,不用div1再去removeChild了,参考之前的‘一个元素不能同时出现在两个地方,除非复制一份’

查、读取属性

// 不同的查询方法,读取到的属性值可能稍有不同

// html
<body>
	<div id="test" class="red border"></div>
    <a id="bd" href="www.baidu.com">百度</a>
</body>

// js
test.classList // 返回一个伪数组:DOMTokenList(2) ["red", "border", value: "red border"]
bd.href // 返回 http://127.0.0.1:8888/www.baidu.com 会自动帮你加上当前的ip地址

test.getAttribute('class') // 返回 red border 
bd.getAttribute('href') // 返回 www.baidu.com 

// 两种方法都可以,只是返回的值形式上稍微有点不同

查父节点(查爸爸)

// node.parentNode 或者 node.parentElement

// 假如div标签里有个p标签,那么想查询p标签的父节点:
p.parentNode 或 p.parentElement

查父节点的父节点(查爷爷)

node.parentElement.parentElement

查子节点

// html:
<div id="test">
        <p id="p1">p标签</p>
      </div>
      
// 两种方法:
test.children //返回一个伪数组:HTMLCollection [p#p1]
test.childNodes //返回一个伪数组:[text, p#p1, text]

使用node.childNodes查询子节点时,空格或者或者也会被认为是一个文本节点,所以两种方法的结果有所不同

查兄弟姐妹节点

//这两种方法都可以,但是要记得返回的伪数组里是包含自己的,所以要记得用remove把自己给排除
node.parentNode.childNodes
node.parentNode.children
查子节点里的老大和老小
// html:
<div id="d1">
        <div id="d2"></div>
        <div id="d3"></div>
        <div id="d4"></div>
    </div>

// 但是空格会影响返回的结果,所以需要改成这样:
<div id="d1"><div id="d2"></div><div id="d3"></div><div id="d4"></div></div>

// 查老大
d1.firstChild // d2

// 查老小
d1.lastChild // d4
查上一个节点和下一个节点
// html内容还和上一个例子一样

// 查上一个节点
d2.previousSibling // d1

// 查下一个节点
d2.nextSibling // d3

// 注意:如果没有是查第一个节点的上一个节点,或者最后一个节点的下一个节点,会返回 null

遍历div里的每一个元素

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

travel(d1,(node)=>{console.log(node)})

DOM操作是跨线程的

浏览器分为渲染引擎和JS引擎,两者各司其职:

渲染引擎只负责操作页面,不能操作JS; JS引擎只负责操作JS,不能操作页面;

而我们使用代码:

document.body.appendChild(div1)

改变了页面,这是为什么?是怎么做到的?

这里用到了跨线程通信:

当浏览器发现JS在文档的body里加了一个div1对象时,就会通知渲染引擎在页面里也新加一个div元素,然后新增的div元素的所有属性都照抄div1d对象的属性;

跨线程通信图示

插入新标签的完整过程

在div1放入页面之前

放入页面前的所有操作,都属于JS线程内的操作

在div1放入页面的过程中

浏览器会发现JS的意图,然后就会通知渲染线程在页面中渲染div1对应的元素

在div1放入页面之后

放入页面后,你在JS线程中对div1的任何操作,都有可能会触发渲染引擎去重新渲染,比如:

div1.id = 'newID'
div1.title = 'newTitle'
div1.style.backgroundColor = 'red'

当然也不是一定就会重新渲染,这个要看具体情况而定。

如果连续对div1进行多次操作,浏览器可能会把多次的操作合并成一次操作,比如:

div.style.backgroundColor = 'green';
div.style.backgroundColor = 'blue';

这样这些的结果是:div不会从原来的背景色变成绿色,然后再变成蓝色,而是直接一次性变成蓝色,这就是浏览器在合并操作;

属性同步

当div1被放入页面以后,在JS线程里,对div1的标准属性进行修改,都会被浏览器同步到页面中,比如id、className、title之类的。另外修改以 “data-” 开头的自定义属性,也会被浏览器同步到页面中;

但是一些非标准属性,就是不是HTML标签的标准属性,比如在JS线程里,给div1对象添加一个x属性,对于这种属性的修改,就不会被同步到页面里了,而是只会停留在JS线程中。

示例代码:

// html:
<div id="test" data-x="hi" x="yes"></div>

// js:
let div1 = document.querySelector('#test');
div1.id = 'test2';
div1.dataset.x = 'hello';
div1.x = 'no';
console.log(div1);

// 返回结果:<div id="test2" data-x="hello" x="yes"></div>
// 只有非标准属性 x 没有同步

过程图示: 启示:如果要用到自定义属性,并且希望改完后被同步到页面里,就要使用 data- 作为前缀。

Property 和 Attribute

property

JS线程里div1对象的所有属性,就叫做div1的property;

attribute

渲染线程中div1对应的div标签里的属性,叫做attribute;

两者区别

两个单词翻译都是属性,但是实质上处于不同的线程,是不同的东西;

大部分时候,同名的 property 和 attribute 的值是相等的,参考图示:

这里的id是标准属性,如果不是标准属性的话,最开始渲染到页面上时,非标准属性的值是相等的,但是在JS线程里操作之后,页面不会同步非标准属性的值,所以后面往往就不相等了,非标准属性只有在最开始的时候相等。

另外 attribute 因为是在页面上,也就是HTML文档里,所以它的属性值只支持字符串;而 property 是在JS线程里,它的属性值可以支持字符串、布尔值等很多类型的值。