Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入浅出DOM基础——《DOM探索之基础详解篇》学习笔记 #9

Open
jawil opened this issue Apr 3, 2017 · 17 comments
Open

Comments

@jawil
Copy link
Owner

jawil commented Apr 3, 2017

      之前通过深入学习DOM的相关知识,看了慕课网DOM探索之基础详解篇这个视频(在最近看第三遍的时候,准备记录一点东西,算是对自己学习的一点总结),对DOM的理解又具体了一步,因为DOM本来就是一个抽象和概念性的东西,每深入一步了解,在脑中就会稍微具体一点,通过这次的对DOM的系统学习,对DOM有一个比较深刻的理解,明白了DOM在JavaScript这门语言中举足轻重的地位,了解了DOm的发展历史,也让我明白了存在浏览器浏览器兼容性的历史原因,对DOM的结构有了进一步的认知,对DOM的一些API也更加熟悉,对比较抽象和概念性的DOM认知稍微具体了一些。下面就是自己深入学习DOM这门课程整理的一些笔记,大部分来自学习中查阅的资料以及视频中老师讲的一些关键性知识点,当然也不可或缺的有自己的一些记录和理解。

原文收录在我的 GitHub博客 (https://github.com/jawil/blog) ,喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴。

文章稍长,本文只论述DOM基础概念,不涉及DOM的一些事件原理机制,页面元素的操作和常用API的讲解以及兼容性事项,所以概念性东西比较多,稍微有点抽象,其中有笔记来大部分来自老师的口述,还有一部分是查阅的文档,最后有一部分是自己的记录和理解。

通过document.createElement("p")创建一个p元素一共溯寻了7层原型链,你知道吗?

学习视频地址:DOM探索之基础详解篇,老师讲的很好,有兴趣的可以结合视频学习一下,建议看完视频再看笔记,加深印象,你会受益匪浅。




1、什么是DOM?

DOM,文档对象模型(Document Object Model)。DOM是 W3C(万维网联盟)的标准,DOM定义了访问HTML和XML文档的标准。在W3C的标准中,DOM是独于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。

W3C DOM由以下三部分组成:

  • 核心DOM - 针对任何结构化文档的标准模型
  • XML DOM - 针对 XML 文档的标准模型
  • HTML DOM - 针对 HTML 文档的标准模型

DOM(文档对象模型)是针对xml经过扩展用于html的应用程序编程接口,我们又叫API。DOM把整个页面映射为一个多层的节点结构,html或xml页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。






2、DOM的地位

我们知道,一个网页是由html来搭建结构的,通过css来定义网页的样式,而JavaScript赋予了页面的行为,通过它我们可以与页面进行交互,实现页面的动画效果等等。那javascript究竟通过什么来实现的呢?通过ECMAScript这个标准,我们可以编写程序让浏览器来解析,利用ECMAScript,我们可以通过BOM对象(即browser object model)来操作浏览器窗口、浏览器导航对象(navigator)、屏幕分辨率(screen)、浏览器历史(history)、cookie等等。但这个通过BOM来实现的交互远远不够。要实现页面的动态交互和效果,操作html才是核心。那如何操作html呢?对,就是DOM,简单的说,DOM给我们提供了用程序来动态控制html的接口,也就是早期的DHTMl的概念。因此,DOM处在javascript赋予html具备动态交互和效果的能力的核心地位上。





3、DOM的发展-DOM0、DOM1、DOM2、DOM3的区别

3.1、DOM0

JavaScript在早期版本中提供了查询和操作Web文档的内容API(如:图像和表单),在JavaScript中定义了定义了'images'、'forms'等,因此我们可以像下这样访问第一张图片或名为“user”的表单:

document.images[0]document.forms['user']

这实际上是未形成标准的试验性质的初级阶段的DOM,现在习惯上被称为DOM0,即:第0级DOM。由于DOM0在W3C进行标准备化之前出现,还处于未形成标准的初期阶段,这时Netscape和Microsoft各自推出自己的第四代浏览器,自此DOM遍开始出各种问题。

3.2、DOM0与DHTML

Netscape Navigator 4和IE4分别发布于1997年的6月和10月,这两种浏览器都大幅扩展了DOM,使JavaScript的功能大大增加,而此时也开始出现一个新名词:DHTML。

DHTML是Dynamic HTML(动态HTML)的简称。DHTML并不是一项新技术,而是将HTML、CSS、JavaScript技术组合的一种描述。即:

  • 利用HTML把网页标记为各种元素
  • 利用CSS设置元素样式及其显示位置
  • 利用JavaScript操控页面元素和样式

利用DHTML,看起来可以很容易的控制页面元素,并实现一此原本很复杂的效果(如:通过改变元素位置实现动画)。但事实并非如此,因为没有规范和标准,两种浏览器对相同功能的实现确完全不一样。为了保持程序的兼容性,程序员必须写一些探查代码以检测JavaScript是运行于哪种浏览器之下,并提供与之对应的脚本。JavaScript陷入了前所未有的混乱,DHTML也因此在人们心中留下了很差的印象。

我们在阅读DOM标准的时候,经常会看到DOM0级这样的字眼,实际上DOM0级这个标准是不存在的。所谓DOM0级只是DOM
历史坐标系中的一个参照点而已,具体地说DOM0级就是指IE4.0和Netscape navigator4.0最初支持的那个DHTML。

3.3、DOM1的出现

在浏览器厂商进行浏览器大站的同时,W3C结合大家的优点推出了一个标准化的DOM,并于1998年10月完成了第一级 DOM,即:DOM1。W3C将DOM定义为一个与平台和编程语言无关的接口,通过这个接口程序和脚本可以动态的访问和修改文档的内容、结构和样式。

DOM1级主要定义了HTML和XML文档的底层结构。在DOM1中,DOM由两个模块组成:DOM Core(DOM核心)和DOM HTML。其中,DOM Core规定了基于XML的文档结构标准,通过这个标准简化了对文档中任意部分的访问和操作。DOM HTML则在DOM核心的基础上加以扩展,添加了针对HTML的对象和方法,如:JavaScript中的Document对象.

3.4、DOM2

在DOM1的基础上DOM2引入了更多的交互能力,也支持了更高级的XML特性。DOM2将DOM分为更多具有联系的模块。DOM2级在原来DOM的基础上又扩充了鼠标、用户界面事件、范围、遍历等细分模块,而且通过对象接口增加了对CSS的支持。DOM1级中的DOM核心模块也经过扩展开始支持XML命名空间。在DOM2中引入了下列模块,在模块包含了众多新类型和新接口:

  • DOM视图(DOM Views):定义了跟踪不同文档视图的接口
  • DOM事件(DOM Events):定义了事件和事件处理的接口
  • DOM样式(DOM Style):定义了基于CSS为元素应用样式的接口
  • DOM遍历和范围(DOM Traversal and Range):定义了遍历和操作文档树的接口

完整的DOM2标准(图片来自百度百科):

3.5、DOM3

DOM3级:进一步扩展了DOM,引入了以统一方式加载和保存文档的方法,它在DOM Load And Save这个模块中定义;同时新增了验证文档的方法,是在DOM Validation这个模块中定义的。

DOM3进一步扩展了DOM,在DOM3中引入了以下模块:

  • DOM加载和保存模块(DOM Load and Save):引入了以统一方式加载和保存文档的方法
  • DOM验证模块(DOM Validation):定义了验证文档的方法
  • DOM核心的扩展(DOM Style):支持XML 1.0规范,涉及XML Infoset、XPath和XML Base







4、认识DOM

DOM可以将任何HTML描绘成一个由多层节点构成的结构。节点分为12种不同类型,每种类型分别表示文档中不同的信息及标记。每个节点都拥有各自的特点、数据和方法,也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形结构。

先看一张w3school上面的一张图:

先来看看下面代码:

<!DOCTYPE html>
  <html>
  <head>
     <meta charset="utf-8">
      <title>DOM</title>
  </head>
  <body>
      <h2><a href="http://www.baidu.com">javascript DOM</a></h2>
      <p>对HTML元素进行操作,可添加、改变或移除css样式等</p>
      <ul>
          <li>Javascript</li>
          <li>DOM</li>
          <li>CSS</li>
      </ul>
  </body>
  </html>

将HTML代码分解为DOM节点层次图:

HTML文档可以说由节点构成的集合,DOM节点有:

  1. 元素节点:上图中<html>、<body>、<p>等都是元素节点,即标签。
  2. 文本节点:向用户展示的内容,如<li>...</li>中的JavaScript、DOM、CSS等文本。
  3. 属性节点:元素属性,如<a>标签的链接属性href="http://www.baidu.com"。






5、文档类型发展史

我们说DOM文档对象模型是从文档中抽象出来的,DOM操作的对象也是文档,因此我们有必要了解一下文档的类型。文档随着历史的发展演变为多种类型,如下:

5.1、GML

GML(Generalized Markup Language, 通用标记语言)是1960年代的一种IBM文档格式化语言,用于描述文档的组织结构、各部件及其相互关系。GML在文档具体格式方面,为文档员提供了一些方便,他们不必再为IBM的打印机格式化语言SCRIPT要求的字体规范、行距以及页面设计等浪费精力。这个IBM的GML包括1960年代的GML和1980年代的ISIL。

5.2、SGML

SGML(Standard Generalized Markup Language, 标准通用标记语言)是1986年基于IBM的GML制定ISO标准(ISO 8879)。SGML是现时常用的超文本格式的最高层次标准,是可以定义标记语言的元语言,甚至可以定义不必采用"<>"的常规方式。由于SGML的复杂,因而难以普及。HTML和XML同样衍生于SGML,XML可以被认为是SGML的一个子集,而HTML是SGML的一个应用。

5.3、HTML

HTML(HyperText Markup Language, 超文本标记语言)是为“网页创建和其它可在网页浏览器中看到的信息”设计的一种标记语言。HTML被用来结构化信息——例如标题、段落和列表等等,也可用来在一定程度上描述文档的外观和语义。1982年,蒂姆·伯纳斯-李为使世界各地的物理学家能够方便的进行合作研究,创建了使用于其系统的HTML。之后HTML又不断地扩充和发展,成为国际标准,由万维网联盟(W3C)维护。第一个正式标准是1995年发布的RFC 1866(HTML 2.0)。

5.4、XML

XML(eXtensible Markup Language, 可扩展标记语言)是专家们使用SGML精简制作,并依照HTML的发展经验,产生出一套使用上规则严谨,但是简单的描述数据语言。XML在1995年开始有雏形,在1998二月发布为W3C的标准(XML1.0)

5.5、XHTML

XHTML(eXtensible HyperText Markup Language, 可扩展超文本标记语言)的表现方式与超文本标记语言(HTML)类似,不过语法上更加严格。从继承关系上讲,HTML是一种基于标准通用标记语言(SGML)的应用,是一种非常灵活的置标语言,而XHTML则基于可扩展标记语言(XML),XML是SGML的一个子集。XHTML 1.0在2000年1月26日成为W3C的推荐标准。





6、DOM节点类型

DOM1级定义了一个Node接口,这个Node接口在javascript中是作为Node类型来实现的。除了IE以外,其他所有浏览器都可以访问这个类型。每个节点都有一个nodeType属性,用于表明节点的类型。节点类型通过定义数值常量和字符常量两种方式来表示,IE只支持数值常量。节点类型一共有12种,这里介绍常用的7种类型。如下图:

看下面这个例子:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>DocumentFragment文档片段节点</title>  
</head>  
<body> 
<!-- tip区域 -->
    <div id="tip">test1</div> 
    <ul class="list-node">
    <li>test2<li>
    </ul>  
    <script>  
        var frag = document.createDocumentFragment();  
        for (var i = 0; i < 10; i++) {  
            var li = document.createElement("li");  
            li.innerHTML = "List item" + i;  
            frag.appendChild(li);  
        }  
        document.getElementById("list-node").appendChild(frag);  
    </script>  
</body>  
</html>  

以下引用均来自老师说的话,感觉每句话都很重要,所以就写下来了。

(1)Element(元素节点):

是组成文档树的重要部分,它表示了html、xml文档中的元素。通常元素因为有子元素、文本节点或者两者的结合,元素节点是唯一能够拥有属性的节点类型。

例子中的:htmlheademetatitlebodydivulliscript都属于Element(元素节点);

(2)Attr(属性节点):

代表了元素中的属性,因为属性实际上是附属于元素的,因此属性节点不能被看做是元素的子节点。因而在DOM中属性没有被认为是文档树的一部分。换句话说,属性节点其实被看做是包含它的元素节点的一部分,它并不作为单独的一个节点在文档树中出现。

例子中的:langcharsetidclass都属于Attr(属性节点);

(3)Text(文本节点):

是只包含文本内容的节点,在xml中称为字符数据,它可以由更多的信息组成,也可以只包含空白。在文档树中元素的文本内容和属性的文本内容都是由文本节点来表示的。

例子中的:DocumentFragment文档片段节点test1test2元素节点之后的空白区域都属于Text(文本节点);

(4)Comment(注释节点):

表示注释的内容

例子中的:<!-- tip区域 -->都属于Comment(注释节点);

(5)Document(文档节点) :

是文档树的根节点,它是文档中其他所有节点的父节点。要注意的是,文档节点并不是html、xml文档的根元素,因为在xml文档中,处理指令、注释等内容可以出现在根元素之外,所以我们在构造DOM树的时候,根元素并不适合作为根节点,因此就有了文档节点,而根元素是作为文档节点的子节点出现的。

例子中的:<!DOCTYPE html>html作为Document(文档节点)的子节点出现;

(6)DocumentType(文档类型节点):

每一个Document都有一个DocumentType属性,它的值或者是null,或者是DocumentType对象。比如声明文档类型时<!doctype html>就是文档类型节点。

例子中的:<!DOCTYPE html> 就属于DocumentType(文档类型节点);





(7)DocumentFragment(文档片段节点):

是轻量级的或最小的Document对象,它表示文档的一部分或者是一段,不属于文档树。不过它有一种特殊的行为,该行为使得它非常有用。比如:当请求把一个DocumentFragment节点插入到文档的时候,插入的不是DocumentFragment自身,而是它的所有的子孙节点。这使得DocumentFragment成了有用的占位符,暂时存放那些一次插入文档的节点,同时它还有利于实现文档的剪切、复制和粘贴等操作。

例子中的:var frag = document.createDocumentFragment(); 就属于DocumentFragment(文档片段节点);





7、DOM的nodeType、nodeName、nodeValue

7.1 nodeType

通过DOM节点类型,我们可知,可以通过某个节点的nodeType属性来获得节点的类型,节点的类型可以是数值常量或者字符常量。示例代码如下:

<!DOCTYPE html>  
<html>  
<head lang="en">  
    <meta charset="UTF-8">  
    <title>nodeType</title>  
</head>  
<body>  
    <div id="container">这是一个元素节点</div>  
    <script>  
        var divNode = document.getElementById('container');  
        /*
        IE中只支持数值常量,因为低版本IE浏览器没有内置Node对象,其他浏览器数值常量和字符常量都支持,因此可
        以直接用数值常量判断,这里为了比较两种写法,便都写在了这里  
        */  
        if (divNode.nodeType == Node.ELEMENT_NODE || divNode.nodeType === 1) {  
            alert("Node is an element.");  
        }         
    </script>  
</body>  
</html>  

7.2 nodeName和nodeValue

先看示例代码:

<!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>  

根据实验,得出以下汇总表格:

8、domReady

还记得刚开始学习JavaScript时候,经常会犯这样的错误:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Dom not ready</title>
    <script>
      document.getElementById("header").style.color = "red";
    </script>
  </head>
  <body>
    <h1 id="header">这里是h1元素包含的内容</h1>
  </body>
</html>

最后发现结果并不是我们想要的,文字并没有变成红色,我想最先入门学习JavaScript操作DOM时候多多少少会遇到这种困惑和错误,其实出现这种问题的原因就是我们没有区分HTML标签和DOM节点的区别的缘故了,由这个问题就引出下面要说的domReady和浏览器渲染解析原理了。

8.1、什么是domReady?

html是一种标记语言,它告诉我们这个页面有什么内容,但行为交互是需要通过DOM操作来实现的。我们不要以为有两个尖括号就以为它是一个DOM了,html标签要通过浏览器解析才会变成DOM节点,当我们向地址栏传入一个url的时候,我们开始加载页面,就能看到内容,在这期间就有一个DOM节点构建的过程。节点是以树的形式组织的,当页面上所有的html都转换为节点以后,就叫做DOM树构建完毕,简称为domReady。

8.2、那么浏览器是如何将html标签解析变成DOM节点的呢?

实际上浏览器是通过渲染引擎来实现的。渲染引擎的职责就是把请求的内容显示到浏览器屏幕上。默认情况下渲染引擎可以显示html、xml文档及图片。通过插件(浏览器扩展)它可以显示其他类型的文档,比如我们安装pdf viewer插件,我们就可以显示pdf文档。这里专注渲染引擎的主要用途,即是将css格式化的html和图片在浏览器上进行显示。

8.3、浏览器渲染引擎的基本渲染流程

浏览器渲染要做的事就是把CSS,HTML,图片等静态资源展示到用户眼前。

渲染引擎首先通过网络获得所请求文档的内容,通常以8k分块的方法来完成:

上图就是html渲染的基本过程,但这并不包含解析过程中浏览器加载外部资源,比如图片、脚本、iframe等的一些过程。说白了,上面的4步仅仅是html结构的渲染过程。而外部资源的加载在html结构的渲染过程中是贯彻始终的,即便绘制DOM节点已经完成,而外部资源仍然可能正在加载或者尚未加载。

8.4、Webkit主要渲染流程

Firefox浏览器Gecko渲染流程跟Webkit内核渲染类似,大同小异,WebKit 和 Gecko 使用的术语略有不同,但整体流程是基本相同的。这里以Webkit内核作为例子来说明浏览器渲染的主要流程。

浏览器的渲染原理并非三言两语,几个图就能说明白的,上图说的只是介绍一个大环节的过程和步骤,这里抛砖引玉象征性说个大概,更多关于浏览器内部工作原理的文章,请阅读:浏览器的工作原理:新式网络浏览器幕后揭秘

8.5、domReady的实现策略

上面的各个代码实例中,并没有考虑domReady,程序也能正常运行,因为我们把javascript代码写在了body元素最后的位置。因为浏览器是从上到下,从左向右渲染元素的,这样实例中的js代码一定在domReady之后去执行的。那为什么还要用domReady呢?事实上,我们在编写大型项目的时候,js文件往往非常多,而且之间会相互调用,大多数都是外部引用的,不把js代码直接写在页面上。这样的话,如果有个domReady这个方法,我们想用它就调用,不管逻辑代码写在哪里,都是等到domReady之后去执行的。

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


你肯定想到了jquery中的$(document).ready(function(){})方法了,其实jquery中的domReady应该和window.onload的实现原理是大同小异的。为了解决window.onload的短板,w3c 新增了一个 DOMContentLoaded 事件。

这里提到了DOMContentLoaded事件,这里由于篇幅有限,就不多做介绍,这里面也有很多细节可以学习,有兴趣的童鞋,可以看看我之前收藏的两篇文章:

你不知道的 DOMContentLoaded
浅谈DOMContentLoaded事件及其封装方法

学习就是一个无底洞,因为深不可测,才让人不断探索。

参考jquery中domReady的实现原理,来看一下javascript中domReady的实现策略。

在页面的DOM树创建完成后(也就是HTML解析第一步完成)即触发,而无需等待其他资源的加载。即domReady实现策略:

1. 支持DOMContentLoaded事件的,就使用DOMContentLoaded事件。
2. 不支持的就用来自Diego Perini发现的著名Hack兼容。兼容原理大概就是通过IE中的document,
documentElement.doScroll('left')来判断DOM树是否创建完毕。

JavaScript实现domReady,【domReady.js】

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();  
            }  
        }  
    }  
}  

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

感兴趣的童鞋可以看看各个主流框架domReady的实现:点击我查看

8.6、一个小栗子看二者差异性

下面通过一个案例,来比较domReady与window.onload实现的不同,很明显,onload事件是要在所有请求都完成之后才执行,而domReady利用hack技术,在加载完dom树之后就能执行,所以domReady比onload执行时间更早,建议采用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>

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

9、元素节点的判断

为什么要判断元素的节点?

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

设计元素类型的判定,这里给出有4个方法:

(1).  isElement  :判定某个节点是否为元素节点
(2).  isHTML     :判定某个节点是否为html文档的元素节点
(3).  isXML       : 判定某个节点是否为xml文档的元素节点
(4).  contains   :用来判定两个节点的包含关系

9.1、元素节点的判定:isElement

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isElement</title>  
</head>  
<body>  
    <div id="test">aaa</div>  
    <!--这是一个注释节点-->  
    <script>  
        var isElement = function (el){  
            return !!el && el.nodeType === 1;  
        }  
        var a = {            //随意定义一个变量,设置nodeType为1  
           nodeType: 1  
        }  
        console.log(isElement(document.getElementById("test")));   
        //结果:  true  
        
        console.log(isElement(document.getElementById("test").nextSibling));
        //这里的nextSibling属性查找下一个相邻节点,即注释节点  
        //结果:  false  
        
        console.log(isElement(a));  
        //结果:  true  
    </script>  
</body>  
</html>  

注意代码中的!!用法:!!一般用来将后面的表达式转换为布尔型的数据(boolean).

因为javascript是弱类型的语言(变量没有固定的数据类型)所以有时需要强制转换为相应的类型,关于JavaScript的隐式转换,可以看看之前我写的一篇博客,这篇文章几乎分析到了所有的转换规则,感兴趣的童鞋可以点击查阅,学习了解一下。

从++[[]][+[]]+[+[]]==10?深入浅出弱类型JS的隐式转换

注意:上面的代码定义了一个变量a,将它的nodeType的值设为1,由于元素节点的节点类型的数值常量为1,所以这里在打印的的时候,会将a认为是元素节点,所以打印true。这种结果明显不是我们想要的,即使这种情况很少出现。下面给出解决方案:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isElement</title>  
</head>  
<body>  
    <div id="test">aaa</div>  
    <!--这是一个注释节点-->  
    <script>  
        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 a = {  
           nodeType: 1  
        }  
        console.log(isElement(document.getElementById("test")));  
        //结果:  true  
        console.log(isElement(document.getElementById("test").nextSibling));  
        //结果:  false  
        console.log(isElement(a));  
        //结果:  false  
    </script>  
</body>  
</html>  

这样,在判断a是否是元素节点时,结果就是false了。

更多关于元素节点的判断请参考:How do you check if a JavaScript Object is a DOM Object?




9.2、HTML文档元素节点的判定和XML文档元素节点的判定:isHTML and isXML

我们可以简单的将所有的元素节点化为两类:一类是HTML,一类是XML。不过从严格意义上来说,HTML只是XML的一个子集,它拥有更多的特性,而XML在矢量绘图的处理上又派生出了两大类:SVG和VML。那么按照这种方法,我们可以简单的认为如果不是HTML,就是XML的元素节点了。而HTML是比较容易识别的,因为它有更多的特性。比如说,XML是没有className的,或者我们通过一个元素的ownerDocument得到它的文档对象,XML是没有document.getElementById()和document.getElementsByTagName()这些方法的.此外,最大的区别是HTML元素的nodeName总是大写的,当你使用createElement()方法创建HTML元素的时候,无论你传入的字母是大写还是小写,最后得到的都是大写。
接下来就看看各大类库是怎么实现HTML和XML文档的元素节点的判定的。

9.2.1、Sizzle, jQuery自带的选择器引擎,判断是否是XML文档
<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isXML</title>  
</head>  
<body>  
    <script>  
        //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方法");  
        }  
    </script>  
</body>  
</html>  

浏览器随便找个HTML页面验证一下:

9.2.1、mootools的slick选择器引擎的源码,判断是否是XML文档
<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isXML</title>  
</head>  
<body>  
    <script>  
        //我们看看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;  
        }  
    </script>  
</body>  
</html>  

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

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isXML</title>  
</head>  
<body>  
    <script>  
        var isXML = function(doc) {  
            return doc.createElement("p").nodeName !== doc.createElement("P").nodeName;  
        }  
          
    </script>  
</body>  
</html>  

我们知道,无论是HTML文档,还是XML文档都支持createELement()方法,我们判定创建的元素的nodeName是区分大小写的还是不区分大小写的,我们就知道是XML还是HTML文档,这个方法是目前给出的最严谨的函数了。

判断是不是HTML文档的方法如下:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isHTML</title>  
</head>  
<body>  
    <script>  
        var isHTML = function(doc) {  
            return doc.createElement("p").nodeName === doc.createElement("P").nodeName;  
        }  
        console.log(isHTML(document));  
    </script>  
</body>  
</html>  

有了以上判断XML和HTML文档的方法,我们就可以实现一个元素节点属于HTML还是XML文档的方法了,实现代码如下:

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>isHTMLElement</title>  
</head>  
<body>  
    <script>  
        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));  
    </script>  
</body>  
</html>  

9.3、判断节点的包含关系

  DOM可以将任何HTML描绘成一个由多层节点构成的结构。节点分为12种不同类型,每种类型分别表示文档中不同的信息及标记。每个节点都拥有各自的特点、数据和方法,也与其他节点存在某种关系。节点之间的关系构成了层次,而所有页面标记则表现为一个以特定节点为根节点的树形结构。DOM间的节点关系大致如下。

  
节点关系不仅仅指元素节点的关系,document文档节点也包含在内。在最新的浏览器中,所有的节点都已经装备了contains()方法,
元素之间的包含关系,用自带的contains方法,只有两个都是元素节点,才能兼容各个浏览器,否则ie浏览器有的版本是不支持的,可以采用hack技术,自己写一个contains方法去兼容。
元素之间的包含关系:contains()方法.

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>contains</title>  
</head>  
<body>  
    <div id="p-node">  
        <div id="c-node">子节点内容</div>  
    </div>  
    <script>  
        var pNode = document.getElementById("p-node");  
        var cNode = document.getElementById("c-node").childNodes[0];  
        alert(pNode.contains(cNode));    //true  
    </script>  
</body>  
</html>  

兼容各浏览器的contains()方法

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>contains</title>  
</head>  
<body>  
    <div id="p-node">  
        <div id="c-node">子节点内容</div>  
    </div>  
    <script>  
        //兼容的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  
    </script>  
</body>  
</html>  





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

10.1、DOM节点继承层次

DOM节点是一个非常复杂的东西,对它的每一个属性的访问,不走运的话,就可能会向上溯寻到N多个原型链,因此DOM操作是个非常耗性能的操作。风头正盛的react为了解决这个问题,提出了虚拟DOM的概念,合并和屏蔽了很多无效的DOM操作,效果非常惊人。接下来看看DOM节点究竟是如何继承的。

10.1.1、创建一个元素节点(Element)的过程

使用document.createElement("p")创建p元素,其实document.createElement("p")是HTMLParagraphElement的一个实例,而HTMLParagraphElement的父类是HTMLElement,HTMLElement的父类是Element,Element的父类是Node,Node的父类是EventTarget,EventTarget的父类是Function,Function的父类是Object。

创建一个p元素一共溯寻了7层原型链:



下面我们来分析一下创建一个元素所继承的属性分别是啥。

1.document.createElement("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

对实例对象,构造函数,以及JavaScript原型链和继承不太熟悉的童鞋,该补习一下基础看看了。


我们先来看看document.createElement("p")自身有哪些属性,遍历对象属性方法一般有三种:

先来讲一讲遍历对象属性三种方法的差异性,当做补充复习。
遍历数组属性目前我知道的有:for-in循环、Object.keys()Object.getOwnPropertyNames(),那么三种到底有啥区别呢?

for-in循环:会遍历对象自身的属性,以及原型属性,包括enumerable 为 false(不可枚举属性);
Object.keys():可以得到自身可枚举的属性,但得不到原型链上的属性;
Object.getOwnPropertyNames():可以得到自身所有的属性(包括不可枚举),但得不到原型链上的属性,Symbols属性
也得不到.

Object.defineProperty顾名思义,就是用来定义对象属性的,vue.js的双向数据绑定主要在gettersetter函数里面插入一些处理方法,当对象被读写的时候处理方法就会被执行了。 关于这些方法和属性的更具体解释,可以看MDN上的解释(戳我);

简单看一个小demo例子加深理解,对于Object.defineProperty属性不太明白,可以看看上面介绍的文档学习补充一下.

'use strict';
class A {
    constructor() {
        this.name = 'jawil';
    }
    getName() {}
}
class B extends A {
    constructor() {
            super();
            this.age = 22;
        }
        //getAge不可枚举
    getAge() {}
        [Symbol('fullName')]() {

        }
}
B.prototype.get = function() {

}
var b = new B();

//设置b对象的info属性的enumerable: false,让其不能枚举.
Object.defineProperty(b, 'info', {
    value: 7,
    writable: true,
    configurable: true,
    enumerable: false
});

//Object可以得到自身可枚举的属性,但得不到原型链上的属性
console.log(Object.keys(b)); //[ 'name', 'age' ]


//Object可A以得到自身所有的属性(包括不可枚举),但得不到原型链上的属性,Symbols属性也得不到
console.log(Object.getOwnPropertyNames(b)); //[ 'name', 'age', 'info' ]

for (var attr in b) {
    console.log(attr);//name,age,get
}

//in会遍历对象自身的属性,以及原型属性
console.log('getName' in b); //true

有了上面的知识作为扩充,我们就可以清晰明了的知道,创建元素P标签每一步都继承了哪些属性,继承对象自身有哪些属性,由于篇幅有限,大家可以自行子在浏览器测试,看看这些对象的一些属性和方法,便于我们理解。

例如我们想看:HTMLElement对象有哪些自身属性,我们可以这么查看:

Object.getOwnPropertyNames(HTMLElement)

我们想看:HTMLElement的原型对象有哪些自身属性,我们可以这么查看:

Object.getOwnPropertyNames(HTMLElement.prototype)

HTMLElement的原型对象有哪些自身属性,根据原型链,我们也可以这么查看:

因为:document.createElement("p").__proto__.__proto__===HTMLElement.prototype

Object.getOwnPropertyNames(document.createElement("p").__proto__.__proto__)
10.1.2、创建一个文本节点(Text)的过程

使用document.createTextNode("xxx")创建文本节点,其实document.createTextNode("xxx")是Text的一个实例,而Text的父类是CharactorData,CharactorData的父类是Node,Node的父类是EventTarget,EventTarget的父类是Function,Function的父类是Object。

创建一个文本节点一共溯寻了6层原型链。

因此,所有节点的继承层次都不简单,但相比较而言,元素节点是更可怕的。从HTML1升级到HTML3.2,再升级到HTML4.1,再到HTML5,除了不断地增加新类型、新的嵌套规则以外,每个元素也不断的添加新属性。
下面看一个例子:创建一个p元素,打印它第一层原型的固有的属性的名字,通过Object的getOwnPropertyNames()获取当前元素的一些属性,这些属性都是他的原始属性,不包含用户自定义的属性。

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>DOM inheritance hierarchy</title>  
</head>  
<body>  
    <script>  
console.log(Object.getOwnPropertyNames(document.createElement("p").__proto__));  
        //访问p元素上一层原型控制台打印:  ["align","constructor"]  
        console.log(
        Object.getOwnPropertyNames(document.createElement("p").__proto__.__proto__)
        );  
        /*访问p元素上一层原型的再上一层原型,控制台会打印很多属性,感兴趣的伙伴可以自己贴代码到控制台看
        一下,它要比访*问第一层原型的属性多得多。这也就是说,每往上一层,原型链就为它添加一些属性。  
        */  
    </script>  
</body>  
</html>  
10.1.3、空的div元素的自有属性

下面看一个空的div元素,并且没有插入到DOM里边,看它有多少自有属性(不包括原型链继承来的属性)

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





10.2、HTML嵌套规则

HTML存在许多种类型的标签,有的标签下面只允许特定的标签存在,这就叫HTML嵌套规则。
不按HTML嵌套规则写,浏览器就不会正确解析,会将不符合嵌套规则的节点放到目标节点的下面,或者变成纯文本。
关于HTML嵌套规则,一定要掌握块状元素和内联元素的区别。

块状元素:一般是其他元素的容器,可容纳内联元素和其他块状元素,块状元素排斥其他元素与其位于同一行,宽度(width)高度(height)起作用。常见块状元素为div和p

内联元素:内联元素只能容纳文本或者其他内联元素,它允许其他内联元素与其位于同一行,但宽度(width)高度(height)不起作用。常见内联元素为a.

块状元素与内联元素嵌套规则:

(1).块元素可以包含内联元素或某些块元素,但内联元素却不能包含块元素,它只能包含其他的内联元素

例:
<div><h1></h1><p></p></div>
<a href="#"><span></span></a>

(2).块级元素不能放在

里面

例:<p><ol><li></li></ol></p><p><div></div></p>

(3).有几个特殊的块级元素提倡只能包含内联元素,不能再包含块级元素,这几个特殊的标签是:

h1、h2、 h3、h4、 h5、 h6、 p 、dt

(4).li标签可以包含div标签

例:
<li><div></div></li>

(5).块级元素与块级元素并列,内联元素与内联元素并列

例:
<div><h2></h2><p></p></div>
<div><a href="#"></a><span></span></div>
@jawil jawil changed the title 深入浅出DOM基础——JavaScript的《DOM探索之基础详解篇》学习笔记 深入浅出DOM基础——《DOM探索之基础详解篇》学习笔记 Apr 4, 2017
@lzlu
Copy link

lzlu commented Apr 5, 2017

提个小意见,作为读者看到这篇文章略长。可以TOC一个目录,脉路清晰点,更容易消化。

@jawil
Copy link
Owner Author

jawil commented Apr 5, 2017

TOC是啥?只看到Segment有这个,github好像没看到自带的这个。@Expelliarmus923

@Eamonnzhang
Copy link

Table of contents @jawil

@jawil
Copy link
Owner Author

jawil commented Apr 5, 2017

github有这个功能吗,应该说是markdown语法,好像文档没看到这个功能介绍。@Eamonnzhang

@BBcaptain
Copy link

BBcaptain commented Apr 5, 2017

可以在chrome商店安装个TOC插件,https://chrome.google.com/webstore/detail/smart-toc/lifgeihcfpkmmlfjbailfpfhbahhibba
如图所示

@jawil
Copy link
Owner Author

jawil commented Apr 5, 2017

感谢推荐,太实用了@BBcaptain

@congFly
Copy link

congFly commented Sep 22, 2017

写的非常好,很受用

@shellphon
Copy link

for-in循环:会遍历对象自身的属性,以及原型属性,包括enumerable 为 false(不可枚举属性);

不可枚举属性不能被for in遍历吧?……例子貌似也没支持这个说法

@jawil
Copy link
Owner Author

jawil commented Sep 24, 2017

const obj = { a: 1, b: 2, c: 3 };

Object.defineProperty(obj, "a", {
  enumerable: false
});

for (let attr in obj) {
  console.log(attr);
}

不好意思,不可枚举属性不能被for in遍历,误导了,应该是'a' in obj是in检测对象属性会遍历对象自身的属性,以及原型属性,包括enumerable 为 false(不可枚举属性);
@shellphon

@yoky
Copy link

yoky commented Jan 22, 2018

博主,请教个问题,判断元素节点类型那一节,什么元素节点操作属性的时候用直接属性,什么时候用方法setAttribute?

@jawil
Copy link
Owner Author

jawil commented Jan 22, 2018

直接属性当然是直接挂在在元素节点上的属性,比如说 ele.textContent,input.vulue,而setAttribute是给元素节点加一个原来没有的属性,元素为了可拓展性,也允许加入自定义的属性。@yoky

举个例子吧,手和脚都是人自带的属性,但是我想飞,我可以去接(setAttribute)一双翅膀😂

@yoky
Copy link

yoky commented Jan 29, 2018

那可以用setAttribute修改本身自带属性的值么?

@jawil
Copy link
Owner Author

jawil commented Jan 29, 2018

写个 demo 试一下不就知道了,这个很好实践的啊,而且印象深刻 @yoky

@yoky
Copy link

yoky commented Feb 23, 2018

确实确实。。比心

@sfsoul
Copy link

sfsoul commented May 25, 2018

hello,针对9.1那块元素节点的判断,也可以用这段代码来判断,我觉得更方便点。
function isElementRight(node){
return window.HTMLElement ? node instanceof window.HTMLElement : !!node && node.nodeType === 1;
}
通过先判断传进来的值是否为HTMLElement的实例,就能过滤掉伪造的普遍对象a啦。

@caihg
Copy link

caihg commented May 25, 2018

@sfsoul 不能过滤这种伪造的元素对象
Object.create(HTMLElement.prototype, {
nodeType: {
value: 1
}
});

@sfsoul
Copy link

sfsoul commented May 25, 2018

@caihg ,感觉你这个例子就太偏激了。。。不过也的确过滤不了。不知道还有没有更好的办法呢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants