DOM((Document Object Model-文档对象模型)

# DOM((Document Object Model-文档对象模型)

[TOC]

# 一、节点的类型

节点的类型 节点的nodeName与nodeValue

# 1.1 元素节点(ELement-1)

  • 拥有子节点和文本,是唯一能拥有属性的节点类型。
  • htmlheadmetatitlebodydivulliscript

# 1.2 属性节点(Attr-2)

  • 元素中的属性,是附属于元素的,是包含它的元素节点的一部分,不属于文档树的一部分。
  • langcharsetidclass

# 1.3 文本节点(Text-3)

  • 只包含文本内容(可以只包含空白)的节点,在xml中称为字符数据。
  • 在文档树中元素的文本内容和属性的文本内容都是由文本节点表示的。
  • 某个节点的空白区域,也是属于文本节点。

# 1.4 注释节点(comment-8)

  • 表示注释的内容。
  • <!-- tip区域 -->

# 1.5 文档节点(document-9)

  • 文档树的根节点,是其他节点的父节点。
  • 注意不是html或者xml的根元素,根元素是作为文档节点的子节点出现的。
  • 整个代码之上看做是文档节点。
  • <!DOCTYPE html>html作为Document(文档节点)的子节点出现。

# 1.6 文档类型节点(documentType-10)

  • 例:<!doctype html>

# 1.7 文档片段节点(documentFragment-11)

  • 文档片段是轻量级的或者是最小的Document 对象,它表示文档的一部分或者是一段,它不属于文档树。
  • 特殊行为:当请求把一个DocumentFragment节点插入到文档的时候,插入的不是DocumentFragment自身,而是它的所有的子孙节点。这使DocumentFragment成了有用的占位符,暂时存放那些一次插入文档的节点,同时它还有利于实现文档的剪切、复制和粘贴等操作。
<!DOCTYPE html>  
<html>  
<head lang="en">  
    <meta charset="UTF-8">  
    <title>nodeName,nodeValue</title>  
</head>  
<body>  
    <!--nodeName,nodeValue实验-->  
    <div id="container">这是一个元素节点</div>  
    <script>  
        var divNode = document.getElementById('container');  
        console.log(divNode.nodeName + "/" + divNode.nodeValue);     
        //结果:    DIV/null  
        
        var attrNode = divNode.attributes[0];  
        console.log(attrNode.nodeName + "/" + attrNode.nodeValue);      
        //结果:   id/container  
        
        var textNode = divNode.childNodes[0];  
        console.log(textNode.nodeName + "/" + textNode.nodeValue);      
        //结果:   #text/这是一个元素节点  
        
        var commentNode = document.body.childNodes[1];  
        //表示取第二个注释节点,因为body下面的第一个注释节点为空白符。  
        console.log(commentNode.nodeName + "/" +commentNode.nodeValue);  
        //结果:  #comment/nodeName,nodeValue实验  
        
        console.log(document.doctype.nodeName + "/" + document.doctype.nodeValue);   
        //结果: html/null  
        
        var frag = document.createDocumentFragment();  
        console.log(frag.nodeName + "/" + frag.nodeValue);    
        //结果: #document-fragment/null  
    </script>  
</body>  
</html>  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 1.8 判断节点类型

  • 建议使用数值常量,而不用字符常量(IE不支持)。
        // 适用于所有浏览器
        if(someNode.nodeType == 1){
            alert('Node is an element.');
        }
1
2
3
4

# 1.9 判断XMLHTML的方法

  • 先使用isElememt判断是否为元素节点,再用creatElement判断元素名大写小写是否都等同,大小写不等同为XML,等同为HTML

# 二、domReady

html标签要通过浏览器解析才会变成DOM节点,当我们向地址栏传入一个url的时候,我们开始加载页面,就能看到内容,在这期间就有一个DOM节点构建的过程。节点是以树的形式组织的,当页面上所有的html都转换为节点以后,就叫做DOM树构建完毕,简称为domReadydomReady 渲染引擎

  • 上面的4步仅仅是html结构的渲染过程,而外部资源的加载在html结构的渲染过程中是贯彻始终的。
    • 即便绘制DOM节点已经完成,而外部资源仍然可能正在加载或者尚未加载。

# 2.1 domReady实现策略

我们在编写大型项目的时候,js文件往往非常多,而且之间会相互调用,大多数都是外部引用的,不把js代码直接写在页面上。这样的话,如果有个domReady这个方法,我们想用它就调用,不管逻辑代码写在哪里,都是等到domReady之后去执行的。

# 2.1.1 window.onload方法

  • 表示当页面所有的元素都加载完毕,并且所有要请求的资源也加载完毕才触发执行function这个匿名函数里边的具体内容。这样肯定保证了代码在domReady之后执行。
  • 使用window.onload方法在文档外部资源不多的情况下不会有什么问题,但是当页面中有大量远程图片或要请求的远程资源时,我们需要让js在点击每张图片时,进行相应的操作,如果此时外部资源还没有加载完毕,点击图片是不会有任何反应的,大大降低了用户体验。

# 2.1.2 DOMContentLoaded事件

  • 解决window.onload的短板

# 2.1.3 DOMReady实现策略

  • 在页面的DOM树创建完成后(也就是HTML解析第一步完成)即触发,而无需等待其他资源的加载。
    1. 支持DOMContentLoaded事件的,就使用DOMContentLoaded事件。
    1. 不支持的就用来自Diego Perini发现的著名Hack兼容。兼容原理大概就是通过IE中的documentdocumentElement.doScroll('left')来判断DOM树是否创建完毕。

具体实现

function myReady(fn){  
    //对于现代浏览器,对DOMContentLoaded事件的处理采用标准的事件绑定方式  
    if ( document.addEventListener ) {  
        document.addEventListener("DOMContentLoaded", fn, false);  
    } else {  
        IEContentLoaded(fn);  
    }  
    //IE模拟DOMContentLoaded  
    function IEContentLoaded (fn) {  
        var d = window.document;  
        var done = false;  
  
        //只执行一次用户的回调函数init()  
        var init = function () {  
            if (!done) {  
                done = true;  
                fn();  
            }  
        };  
        (function () {  
            try {  
                // DOM树未创建完之前调用doScroll会抛出错误  
                d.documentElement.doScroll('left');  
            } catch (e) {  
                //延迟再试一次~  
                setTimeout(arguments.callee, 50);  
                return;  
            }  
            // 没有错误就表示DOM树创建完毕,然后立马执行用户回调  
            init();  
        })();  
        //监听document的加载状态  
        d.onreadystatechange = function() {  
            // 如果用户是在domReady之后绑定的函数,就立马执行  
            if (d.readyState == 'complete') {  
                d.onreadystatechange = null;  
                init();  
            }  
        }  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

在页面中引入donReady.js文件,引用myReady(回调函数)方法即可。

# 2.1.4 domReadywindow.onload的差异

webkit主要渲染流程 - onload事件是要在所有请求都完成之后才执行。 - domReady利用hack技术,在加载完dom树之后就能执行。 - domReadyonload执行时间更早,建议采用domReady

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="utf-8">
    <title>domReady与window.onload</title>
    <script src="domReady.js"></script>
</head>

<body>
    <div id="showMsg"></div>
    <div>
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zofelhdj20xc0xc42s.jpg" alt="">
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zofahw3j20m80etq4a.jpg" alt="">
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zoi3ny6j20l20dw4gd.jpg" alt="">
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zog3tauj20m80et0uw.jpg" alt="">
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zofi2o5j20m80ettaq.jpg" alt="">
        <img src="http://ww1.sinaimg.cn/large/ae49ba57gy1fe9zohjuvhj20tb0cdwvp.jpg" alt="">
    </div>
    <script>
    var d = document;
    var msgBox = d.getElementById("showMsg");
    var imgs = d.getElementsByTagName("img");
    var time1 = null,
        time2 = null;
    myReady(function() {
        msgBox.innerHTML += "dom已加载!<br>";
        time1 = new Date().getTime();
        msgBox.innerHTML += "时间戳:" + time1 + "<br>";
    });
    window.onload = function() {
        msgBox.innerHTML += "onload已加载!<br>";
        time2 = new Date().getTime();
        msgBox.innerHTML += "时间戳:" + time2 + "<br>";
        msgBox.innerHTML += "domReady比onload快:" + (time2 - time1) + "ms<br>";
    };
    </script>
</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

执行结果对比,发现DomReadyonload快乐2秒多。

# 三、元素节点的判断

  • 为什么要判断元素节点
    • 因为属性的一系列操作与元素的节点类型息息相关,如果我们不区分它们,我们就不知道用元素的直接属性操作(ele.xxx=yyy)还是用一个方法操作(el.setAttribute(xxx,yyy))。

# 3.1 元素节点的判定:isElement

        var isElement = function (el){  
            return !!el && el.nodeType === 1;  
        }  
        //!!一般用来将后面的表达式转换为布尔型的数据(boolean)。
        //因为javascript是弱类型的语言(变量没有固定的数据类型,所以有时需要强制转换为相应的类型。
1
2
3
4
5

# 3.2 HTML文档元素节点的判定和XML文档元素节点的判定:isHTMLandisXML

  • Sizzle, jQuery自带的选择器引擎,判断是否是XML文档
        //Sizzle, jQuery自带的选择器引擎  
        var isXML = function(elem) {  
            var documentElement = elem && (elem.ownerDocument || elem).documentElement;  
            return documentElement ? documentElement.nodeName !== "HTML" : false;  
        };  
        console.log(isXML(document.getElementById("test")));  
  
        //但这样不严谨,因为XML的根节点,也可能是HTML标签,比如这样创建一个XML文档  
        try {  
            var doc = document.implementation.createDocument(null, 'HTML', null);  
            console.log(doc.documentElement);  
            console.log(isXML(doc));  
        } catch (e) {  
            console.log("不支持creatDocument方法");  
        }  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • mootools的slick选择器引擎的源码,判断是否是XML文档
        //我们看看mootools的slick选择器引擎的源码:  
        var isXML = function(document) {  
            return (!!document.xmlVersion) || (!!document.xml) || (toString.call(document) == '[object XMLDocument]')  
                    || (document.nodeType == 9 && document.documentElement.nodeName != 'HTML');  
        };  
  
        //精简版  
        var isXML = window.HTMLDocument ? function(doc) {  
            return !(doc instanceof HTMLDocument);  
        } : function(doc) {  
            return "selectNodes" in doc;  
        }  
1
2
3
4
5
6
7
8
9
10
11
12

不过,这些方法都只是规范,javascript对象是可以随意添加的,属性法很容易被攻破,最好是使用功能法。功能法的实现代码如下:

        var isXML = function(doc) {  
            return doc.createElement("p").nodeName !== doc.createElement("P").nodeName;  
        }
1
2
3

# 3.2.1 最严谨的函数方法

无论是HTML文档,还是XML文档都支持createELement()方法,我们判定创建的元素的nodeName是区分大小写的还是不区分大小写的,我们就知道是XML还是HTML文档。

       var isHTML = function(doc) {  
            return doc.createElement("p").nodeName === doc.createElement("P").nodeName;  
        }  
        console.log(isHTML(document));  
1
2
3
4
  • 实现一个元素节点属于HTML还是XML文档的方法
        var testDiv = document.createElement('div');  
        var isElement = function (obj) {  
            if (obj && obj.nodeType === 1) {//先过滤最简单的  
                if( window.Node && (obj instanceof Node )){ 
                //如果是IE9,则判定其是否Node的实例  
                    return true; //由于obj可能是来自另一个文档对象,因此不能轻易返回false  
                }  
                try {//最后以这种效率非常差但肯定可行的方案进行判定  
                    testDiv.appendChild(obj);  
                    testDiv.removeChild(obj);  
                } catch (e) {  
                    return false;  
                }  
                return true;  
            }  
            return false;  
        }  
        var isHTML = function(doc) {  
            return doc.createElement("p").nodeName === doc.createElement("P").nodeName;  
        }  
        var isHTMLElement = function(el){  
           if(isElement){  
              return isHTML(el.ownerDocument);  
           }  
           return false;  
        }  
        console.log(isHTMLElement(testDiv));  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 3.3 判定两个节点的包含关系:contains

在最新的浏览器中,所有的节点都已经装备了contains()方法。

  • 元素之间的包含关系,用自带的contains方法,只有两个都是元素节点,才能兼容各个浏览器,否则ie浏览器有的版本是不支持的,可以采用hack技术,自己写一个contains方法去兼容。
        //兼容的contains方法  
        function fixContains(a, b) {  
            try {  
                while ((b = b.parentNode)){  
                    if (b === a){  
                        return true;  
                    }  
                }  
                return false;  
            } catch (e) {  
                return false;  
            }  
        }  
        var pNode = document.getElementById("p-node");  
        var cNode = document.getElementById("c-node").childNodes[0];  
        alert(fixContains(pNode, cNode));        //true  
        alert(fixContains(document, cNode));     //true  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 四、DOM节点继承层次与嵌套规则

DOM节点是一个非常复杂的东西,对它的每一个属性的访问,不走运的话,就可能会向上溯寻到N多个原型链,因此DOM操作是个非常耗性能的操作。

# 4.1DOM节点继承层次

# 4.1.1创建一个元素节点Element的过程

  • 使用document.createElement("p")创建p元素
    • document.createElement("p")HTMLParagraphElement的一个实例
function HTMLParagraphElement() { 
    [native code] 
}
document.createElement("p")=new HTMLParagraphElement('p');

//document.createElement("p").constructor===HTMLParagraphElement //document.createElement("p").__proto__===HTMLParagraphElement.prototype
1
2
3
4
5
6
- `HTMLParagraphElement`的父类是`HTMLElement`
- `HTMLElement`的父类是`Element`
- `Element`的父类是`Node`
- `Node`的父类是`EventTarget`
- `EventTarget`的父类是`Function`
- `Function`的父类是`Object`
  • Object.getOwnPropertyNames()可以得到自身所有的属性(包括不可枚举),但得不到原型链上的属性,Symbols属性也得不到。
// 查看HTMLElement对象的自身属性
Object.getOwnPropertyNames(HTMLElement)
// 查看HTMLElement的原型对象的自身属性
Object.getOwnPropertyNames(HTMLElement.prototype)
// 根据原型类,查看HTMLElement的原型对象的自身属性
// document.createElement("p").__proto__.__proto__===HTMLElement.prototype
Object.getOwnPropertyNames(document.createElement("p").__proto__.__proto__)
1
2
3
4
5
6
7
  • 创建一个p元素,打印它第一层原型的固有的属性的名字,通过Object的getOwnPropertyNames()获取当前元素的一些属性,这些属性都是他的原始属性,不包含用户自定义的属性。
console.log(Object.getOwnPropertyNames(document.createElement("p").__proto__));  
// 访问p元素上一层原型控制台打印:  ["align","constructor"]  
console.log(
    Object.getOwnPropertyNames(document.createElement("p").__proto__.__proto__)
);  
// 访问p元素上一层原型的再上一层原型,控制台会打印很多属性。
// 它要比访问第一层原型的属性多得多。这也就是说,每往上一层,原型链就为它添加一些属性。
1
2
3
4
5
6
7

即便是一个空的div元素,自身也有很多属性。

# 4.1.2创建一个文本节点Text的过程

  • document.createTextNode("xxx")创建文本节点
    • document.createTextNode("xxx")Text的一个实例
    • Text的父类是CharactorData
    • CharactorData的父类是Node
    • Node的父类是EventTarget
    • EventTarget的父类是Function
    • Function的父类是Object。 创建一个文本节点一共溯寻了6层原型链。

”在新的HTML规范中,许多元素的固有属性(比如value)都放到了原型链当中,数量就更加庞大了。因此,未来的发展方向是尽量使用现成的框架来实现,比如MVVM框架,将所有的DOM操作都转交给框架内部做精细处理,这些实现方案当然就包括了虚拟DOM的技术了。但是在使用MVVM框架之前,掌握底层知识是非常重要的,明白为什么这样做,为什么不这样做的目的。这也是为什么要理解DOM节点继承层次的目的。“

# 五、常用的DOM操作

# 5.1 获取 & 增删查改

  • 获取
var element = document.querySelector('#test');
1
element.appendChild(Node);
1
element.removeChild(Node);
1
element.nextSibling // 获取元素之后的兄弟节点, 会拿到注释文本,空白符这些
element.nextElementSibling  // 等同, 获取标签(不会拿到注释文本这些)

element.previousSibling // 和上面同理,往前找兄弟节点
element.previousElementSibling
1
2
3
4
5
element.setAttribute(name, value); // 增加属性
element.removeAttribute(attrName); //删除属性
1
2
  • 不使用innerHTML来实现以下代码
<div id="test">
   <span>Hello, World</span>
</div>
1
2
3
var test = document.createElement('div');  // 创建一个块级元素
test.setAttribute("id","test"); // 设置其id 属性
var span = document.createElement('span'); // 创建一个 span
span.innerText = "Hello,world"; // 插入 span 的文本内容
test.appendChild(span); // 组合节点

element.appendChild(test); //追加到某个节点区域
1
2
3
4
5
6
7

# 5.2 获取与设置属性值

# 5.1 style(各大浏览器都兼容,但只能获取行内样式)

获取不了外部样式,如果行内没有样式则返回空值。

  • 获取值:ele.style.attr()
  • 设置值:ele.style.attr='值'
  • e.g. ele.style.border

function css(obj, attr) {

# 5.2 currentStyle(只兼容IE,只能获取不能设置)

if (obj.currentStyle) {
    return obj.currentStyle[attr];
} else {
1
2
3

# 5.3 getComputedStyle(只兼容火狐谷歌,只能获取不能设置)

		return getComputedStyle(obj, false)[attr];
	}
}
1
2
3

# 5.4 object.getAttribute(attribute)(获取)

  • 只有一个参数——你打算查询的属性的名字:
  • 不过,getAttribute()方法不能通过document对象调用,只能通过一个元素节点对象调用它。

# 5.5 obiect.setAttribute(attribute,value)设置

  • 这里有一个非常值得关注的细节
    • 通过setAttribute()方法对文档做出的修改,将使得文档在浏览器窗口里的显示效果和/或行为动作发生相应的变化。
    • 但我们在通过浏览器的view source(查看源代码)选项去查看文档的源代码时看到的仍将是原来的属性值。
  • setAttribute()方法做出的修改不会反映在文档本身的源代码里。
    • 这种“表里不一”的现象源自DOM的工作模式:
      1. 先加载文档的静态内容。
      2. 再以动态方式对它们进行刷新,动态刷新不影响文档的静态内容。
  • 这正是DOM的真正威力和诱人之处:对页面内容的刷新不需要最终用户在他们的浏览器里执行页面刷新操作就可以实现。

# 5.6 object.getBoundingClientRect()返回元素的大小及其相对于视口的位置

DOMRect {x: 8, y: 8, width: 300, height: 300, top: 8, …}
bottom: 308
height: 300left: 8
right: 308top: 8
width: 300
x: 8
y: 8
__proto__: DOMRect
1
2
3
4
5
6
7
8