1、DOM
DOM(Document Object Model)即文档对象模型,是从文档中抽象出来的,DOM 操作的对象就是文档,DOM 将 HTML 文档呈现为带有元素、属性和文本的树结构,即节点树。通过 DOM,JS 可创建动态的 HTML,可以使网页显示动态效果并实现与用户的交互功能。DOM 给我们提供了用程序来动态控制 HTML 的接口(也叫 API),因此 DOM 处在 JS 赋予 HTML 具备动态交互和效果能力的核心地位上。想要安全的操作 DOM,必须等到页面中所有的 HTML 都解析成 DOM 节点,才能进行操作,因此我们必须了解 DOMReady。在这之前我们先来回顾一下 DOM节点。
(1)、常见的节点类型
常见的节点类型有以下 7 种:
节点类型 | 说明 | 数值常量 |
Element(元素节点) | HTML标签元素。 | 1 |
Attr(属性节点) | 元素节点的属性。 | 2 |
Text(文本节点) | 元素节点或属性节点中的文本内容。 | 3 |
Comment(注释节点) | 表示注释的内容。 | 8 |
Document(文档节点) | 表示整个文档(DOM 树的根节点,即 document )。 | 9 |
DocumentType(文档类型节点) | <!DOCTYPE html>就是文档类型节点。 | 10 |
DocumentFragment(文档片段节点) | 表示文档的一部分或者是一段,它不属于文档树。 | 11 |
(2)、节点类型说明
元素节点,就是 HTML 标签元素,如 <div> 、 <p>、<ul> 等。
属性节点,就是元素节点的属性,如 id 、class 、name 等。属性节点不能被看作是元素节点,因而在 DOM 中属性没有被认为是文档树的一部分,换句话就是说属性节点是包含他的元素节点的一部分,他并不作为一个单独的节点在文档树中出现。
文本节点,就是只包含文本内容的节点。可以包含更多信息,也可以只包含空白,在文档树中元素的文本内容和属性的文本内容都是由文本节点来表示的。
注释节点,就是文档中的注释,其形式为 <!-- 这是一个注释 -->。
文档节点,就是整个文档,是文档树的根节点,是文档中所有其他节点的父节点。这里需要注意:文档节点并不是 HTML 文档的根元素,在构造 DOM 树时,根元素并不适合作为根节点,于是就有了文档节点,而根元素是作为文档节点的子节点出现的。将整个 HTML 文档代码之上看为一个文档节点,这个节点下包含一个文档类型节点 <!DOCTYPE html> 和一个元素节点 <html>,两个子节点。
文档类型节点,每一个 Document 都有一个 DocumentType 属性,<!DOCTYPE html> 就是文档类型节点。
文档片段节点,是轻量级的或最小的 Document 对象,它表示文档的一部分或者是一段,它不属于文档树。不过它有一种特殊的行为,这一行为非常有用,比如当请求把一个DocumentFragment 节点插入到文档的时候,插入的不是 DocumentFragment 自身,而是它所有的子孙节点。这时 DocumentFragment 就成了有用的占位符,暂时存放那些依次插入文档的节点,同时它还有利于实现文档的剪切、复制、粘贴等操作。像 JS 代码中插入元素所定义的变量,这个变量只是作为一个临时的占位符。这就是所谓的文档片段节点。
文档片段也叫文档碎片,创建一个文档碎片可使用 document.createDocumentFragment() 方法,可直接给父节点下插入 n 个子节点。在理论上文档碎片可以提高 DOM 操作性能。文档碎片在低版本浏览器可以大大提高页面性能,但是在高级的浏览器,几乎性能没有提高,而且还会影响性能。所以不建议使用,这东西基本是已经淘汰了。下面是创建文档碎片的实例。
实例:在 ul 元素下插入一个文档碎片
1 <body> 2 <ul id="listNode"></ul> 3 4 <script> 5 //创建一个文档碎片 6 var frag = document.createDocumentFragment(); 7 //使用循环设置创建10个li元素 8 for(var i = 0; i < 10; i++){ 9 //创建一个li元素 10 var oLi = document.createElement('li'); 11 //li元素显示的内容 12 oLi.innerHTML = 'list ' + i; 13 //创建完毕后插入文档碎片中 14 frag.appendChild(oLi); 15 } 16 //最后将文档碎片插入到父节点ul元素下 17 document.getElementById('listNode').appendChild(frag); 18 </script> 19 </body>
(3)、节点类型、节点名称和节点值
属性的一系列操作是与元素的类型息息相关的,如果我们不对元素的节点类型作判断,就不知道如何操作:例如:obj.xx = yy 还是 obj.setAttribute(xx, yy),setAttribute() 方法可添加一个新属性并指定值,或者把一个现有的属性设定为指定的值,如果我们知道节点的类型,就可以直接设置或者是使用方法设置,所以我们有必要判断节点的类型,避免耗费资源,造成意想不到的结果。判断节点类型可使用 nodeType 属性用数值常量进行判断,其操作很简单,就是判断某个节点的节点类型是否等于该节点类型的数值常量。
实例:节点类型判断
1 <body> 2 <!-- 这是一个注释。 --> 3 <div id="div1">这是一个div元素节点。</div> 4 5 <script> 6 //判断是否为注释节点 7 var commentNode = document.body.childNodes[1]; 8 if(commentNode.nodeType == 8){ 9 alert('该节点是注释节点。'); 10 } 11 12 //判断是否为元素节点 13 var divNode = document.getElementById('div1'); 14 if(divNode.nodeType == 1){ 15 alert('该节点是元素节点。'); 16 } 17 18 //判断是否为文本节点 19 var textNode = document.getElementById('div1').childNodes[0]; 20 if(textNode.nodeType == 3){ 21 alert('该节点是文本节点。'); 22 } 23 </script>
其实我们在获取节点的子节点时,不使用 childNodes 属性,而使用 children 属性,就可以避免这一问题,因为通过 childNodes 属性返回的是子节点集合,不仅包括元素节点,而且还包括文本节点,浏览器会将标签之间的空白默认为文本节点,而使用 children 属性,则只返回元素节点,不包括文本节点,还不包括注释节点。
节点名称和节点值,直接使用实例演示:
1 <body> 2 <!-- nodeName 和 nodeValue 演示 --> 3 <div id="div1">节点名称和节点值。</div> 4 <script> 5 //元素节点 = nodeType:1 6 var divNode = document.getElementById('div1'); 7 console.log('元素节点的节点名称和值:' + divNode.nodeName + '/' + divNode.nodeValue); 8 //nodeName:返回 元素的标签名(DIV)全部大写。nodeValue:返回 null 9 10 //属性节点 = nodeType:2 11 var attrNode = divNode.attributes[0]; 12 console.log('属性节点的节点名称和值:' + attrNode.nodeName + '/' + attrNode.nodeValue); 13 //nodeName:返回 属性的名称(id)。nodeValue:返回 属性的值(div1) 14 15 //文本节点 = nodeType:3 16 var textNode = divNode.childNodes[0]; 17 console.log('文本节点的节点名称和值:' + textNode.nodeName + '/' + textNode.nodeValue); 18 //nodeName:返回 #text。nodeValue:返回 节点所包含的文本 19 20 //(comment)注释节点 = nodeType:8 21 var commentNode = document.body.childNodes[1]; 22 console.log('注释节点的节点名称和值:' + commentNode.nodeName + '/' + commentNode.nodeValue); 23 //nodeName:返回 #comment。nodeValue:返回 注释的内容 24 25 //(DocumentType)文档类型节点 = nodeType:10 26 console.log('文档类型节点的节点名称和值:' + document.doctype.nodeName + '/' + document.doctype.nodeValue); 27 //nodeName:返回 document的名称(html)。nodeValue:返回 null 28 29 //(DocumentFragment)文档片段节点 = nodeType:11 30 var farg = document.createDocumentFragment(); 31 console.log('文档片段节点的节点名称和值:' + farg.nodeName + '/' + farg.nodeValue); 32 //nodeName:返回 #document-fragment。nodeValue:返回 null 33 </script>
2、DOMReady
(1)、JS 在页面中的位置
页面中的 JS 代码,可以引入外部的 JS 文件,也可以放在 <head> 标签中或 <body> 标签中,放在 body 中的 JS 会在页面加载的时候被执行,而 head 中的 JS 会在被调用的时候才执行。
放在 <head> 标签中或 <body> 标签中 的区别:
浏览器解析 HTML 文档是从上到下、从左到右依次进行的,如果把 JS 放在 head 里的话,则先被解析,但这时候 body 还没有被解析,所以会返回空值,也就是会出错。放在 head 中的 JS 代码会在页面加载完成之前就读取,而放在 body 中的 JS 代码,会在整个页面加载完成之后读取。这就说明了,如果我们想定义一个全局对象,而这个对象是页面中的某个按钮时,我们必须将其放入 body 中,道理很明显:如果放入head,那当你定义的时候,那个按钮都没有被加载,可能获得的是一个 undefind。
脚本应该放置的位置:
页面中的 JS 会在浏览器加载页面的时候被立即执行,有时候我们想让一段脚本在页面加载的时候执行,而有时候我们想在用户触发一个事件的时候执行脚本。
需调用才执行的脚本或事件触发执行的脚本放在 HTML 的 head 部分中,head 部分中的脚本,可以保证脚本在任何调用之前被加载。
当页面被加载时执行的脚本放在 HTML 的 body 部分。放在 body 部分的脚本通常被用来生成页面的内容。
body 和 head 部分可同时有脚本:文件中可以在 body 和 head 部分同时存在脚本。
外部脚本:
有时候需要在几个页面中运行同样的脚本程序, 这时就需要用到外部脚本,而不需要在各个页面中重复的写这些代码。
(2)、JS 在页面中的应用
①、放在 body 部分中:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 </head> 7 <body> 8 <h1 id="main">这是h1标题中的一些文本。</h1> 9 <script> 10 document.getElementById('main').style.color = 'red'; 11 </script> 12 </body> 13 </html>
上面的实例,通过 JS 改变了 h1 标题的 color 属性,当打开页面时标题显示为红色。
②、放在 head 部分中:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <script> 7 document.getElementById('main').style.color = 'red'; 8 </script> 9 </head> 10 <body> 11 <h1 id="main">这是h1标题中的一些文本。</h1> 12 </body> 13 </html>
将同样的 JS 代码放在 head 部分中,就无法正常运行了,浏览器报错:未捕获的类型错误:不能读取一个空元素的样式属性。出现这样的错误就是没有分清 HTML标签和 DOM 节点之间的区别,HTML 是超文本标记语言,用于展示内容,而行为交互是需要通过 DOM 操作来实现的,HTML 标签要通过浏览器解析,才能变成 DOM 节点,当我们向地址栏输入一个 URL 时,开始加载页面,然后就能够看到内容,在这期间就有一个 DOM 节点构建的过程,节点是以树的形式组织的。JS 对于 DOM 的操作必须在 DOM 树构建完毕后,而上面的实例,head 部分中的 JS 会被浏览器先解析,但是这时候 body 中的元素还没有被解析,DOM树并没有构建完毕,所以就出事了。那如果就想把 JS 代码放在 head 部分中,还不想出错,该怎么解决呢?下文再做具体分析。
(3)、DOMReady
HTML 标签需要通过浏览器解析才会变成 DOM 节点,在刷新 URL 地址的时候就有 DOM 节点的构建过程,当页面上所有的 HTML 标签都转换为节点以后,DOM 树才构建完毕,这就简称为 DOMReady。
那浏览器是如何将 HTML 标签解析变成节点的呢,浏览器是通过渲染引擎来实现的,渲染引擎的职责就是渲染,即把请求的 HTML 内容显示到浏览器屏幕上。所谓渲染,就是浏览器把请求到的 HTML 内容显示在屏幕上的过程,所谓渲染引擎,就是浏览器的内核,是浏览器最核心的东西。
各大浏览器的渲染引擎:
Firefox、Chrome 和 Safari 是基于两种渲染引擎构建的:
Firefox 使用 Geoko 内核,是 Mozilla 自主研发的渲染引擎,Gecko 是开源引擎,Gecko 也是一个跨平台内核,可以在Windows、Linux和Mac OS X等主要操作系统中运行。
Safari 和 Chrome 都使用 WebKit 内核,WebKit 和 WebCore 均是 KHTML 的衍生产品,KHTML 是 HTML 页面渲染引擎之一,由 KDE 所开发。KHTML 拥有速度快捷的优点,对错误语法的容忍度要比 Mozilla 产品所使用的 Gecko 引擎小。WebKit 是一款开源渲染引擎,他本来是为 linux 平台研发的,后来由苹果公司移植到 Mac 及 Windows上使用。
而 IE 使用的是 Trident 内核(又称为MSHTML),是微软的视窗操作系统 (Windows) 搭载的网页浏览器 Internet Explorer 使用的渲染引擎,该内核程序在1997年的 IE 4 中首次被采用,之后不断地加入新的技术并随着新版本的 IE 发布,但是 Trident 只能用于 Windows 平台。
渲染引擎在取得内容之后的基本流程:
解析 HTML 以构建 DOM 树 -> 构建 render 树 -> 布局 render 树 -> 绘制 render 树
渲染引擎开始解析 HTML,并将标签转化为一棵DOM树,接着,他解析外部 CSS 文件及 style 标签中的样式信息,这些样式信息以及 HTML 中的可见性指令将被用来构建另一棵树 —— render 树,用于渲染 DOM 树的树 —— 渲染树(render tree)。
render 树由一些包含颜色和大小等属性的矩形组成,他将被按照正确的顺序显示到屏幕上。
render 树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标。再下一步就是绘制,即遍历 render 树,并使用 UI 后端层绘制每个节点。
值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 HTML 都解析完成之后再去构建和布局render 树。他是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
浏览器打开页面的常规流程:
①、浏览器下载的顺序是从上到下,渲染的顺序也是从上到下,下载和渲染是同时进行的。
②、在渲染到页面的某一部分时,其上面的所有部分都已经下载完成(并不是说所有相关联的元素都已经下载完)。
③、如果遇到语义解释性的标签嵌入文件(JS 脚本,CSS 样式),那么此时IE的下载过程会启用单独连接进行下载。
④、并且在下载后进行解析,解析过程中,停止页面所有往下元素的下载。
⑤、样式表在下载完成后,将和以前下载的所有样式表一起进行解析,解析完成后,将对此前所有元素(含以前已经渲染的)重新进行渲染。
⑥、JS、CSS中如有重复定义,则之后定义的函数将覆盖前面定义的函数。
3、DOMReady 实现
脚本在 HTML DOM 加载渲染布局显示完成后才能运行,且要放在 body 部分内容的后面,而 DOMReady 则定义使脚本无论放在哪里都能执行。
(1)、使用定时器
把 JS 代码放在 head 部分中,还不想出错,就是等 DOM 树构建完毕后,再执行脚本,那么是否可以使用定时器延迟脚本的执行,避免这个错误呢。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JavaScript实例</title> <script> //在DomReady完毕后2秒执行。打开页面可以看到一个样式变化的过程。 setTimeout(function (){ document.getElementById('main').style.color = 'red'; },2000); </script> </head> <body> <h1 id="main">这是h1标题中的一些文本。</h1> </body> </html>
测试上面的代码,可以看到,在打开页面时标题显示为黑色,过一会后才会转变为红色,虽然这样不会报错了,但这并不是我们想要的效果。那可以再将延迟时间缩短,不就解决了吗,我们将时间设置为 30 毫秒再试试。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <script> 7 //将事件缩至30毫秒 8 setTimeout(function (){ 9 document.getElementById('main').style.color = 'red'; 10 },30); 11 </script> 12 </head> 13 <body> 14 <h1 id="main">这是h1标题中的一些文本。</h1> 15 </body> 16 </html>
测试上面代码,在打开页面时显示为红色,但如果刷新页面的话,还是能看到黑色的闪动了一下,虽然无伤大雅,但这样还存在着一个很严重的问题,如果 DomReady 时间超过了 30 毫秒,那还是会出错,显然这方法是不可行的。
(2)、使用 window.onload
window.onload 事件是在浏览器绘制完 DOM 节点,再加载完页面上的所有资源后,然后执行脚本。也就是说在文档解析渲染,资源加载完成之前,不让脚本执行。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <script> 7 window.onload = function (){ 8 document.getElementById('main').style.color = 'red'; 9 }; 10 </script> 11 </head> 12 <body> 13 <h1 id="main">这是h1标题中的一些文本。</h1> 14 </body> 15 </html>
测试上面的代码,打开页面后显示为红色,再刷新也不会出现黑色的闪动,显然该方法是可行的。如果把 JS 代码放在 head 部分中,一般情况下都需要绑定一个监听,即window.onload 事件,等全部的 HTML 文档渲染完成后,再执行代码,这样就妥妥的了。
该方法在文档外部资源不多的情况下,是没什么问题,但如果网站有很多图片,我们要用 JS 做到在点击每张图片时弹出图片的 src 属性,这时候就有问题了,而且是出大事了,我们都知道 DOM 树很快就构建完成了,但是这么多图片还在缓慢的加载中,想要先执行 JS 的效果,就得等到所有的图片全部加载完毕后才能实现,而在这期间页面不会响应用户任何操作,浏览器就跟死了一般。所以使用 window.onload 对于很多实际的操作(比如DOM操作,事件绑定等)就显得太迟了,比如图片过多,window.onload 却迟迟不能触发,影响用户体验。而 DOMReady 就可以满足提前绑定事件的需求。
(3)、jQuery 实现 DOMReady
最简单的方法就是使用 jQuery,jQuery 是一个 JS 函数库,封装了大量的 JS 方法,使用 jQuery 可以极大地简化 JS 编程。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script> 7 <script> 8 //jQuery 实现 9 $(document).ready(function (){ 10 document.getElementById('main').style.color = 'red'; 11 }); 12 </script> 13 </head> 14 <body> 15 <h1 id="main">这是h1标题中的一些文本。</h1> 16 </body> 17 </html>
(4)、JS 实现 DOMReady
用 JS 实现 DOMReady 其实也很简单,可以添加一个监听事件,即 addEventListener,该方法的语法为:document.addEventListener("事件名称", 函数, false);,false 表示在冒泡阶段捕获。再传入DOMContentLoaded 事件,这个事件是从 onload 事件延伸而来的,当一个页面完成加载时,初始化脚本的方法是使用 onload 事件,该方法的缺点是仅在所有资源都完全加载后才被触发,如果页面的图片很多的话,从用户访问到 onload 触发可能需要较长的时间,所以开发人员随后创建了一种自定义事件,DOMReady,他在 DOM 构建之后、资源加载之前就可以被触发,他的表现形式就是 DOMContentLoaded 。jQuery 源码中也是使用该方法完成的。
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>JavaScript实例</title> 6 <script> 7 function domReady(fn){ 8 document.addEventListener('DOMContentLoaded', fn, false); 9 } 10 11 domReady(function (){ 12 document.getElementById('main').style.color = 'red'; 13 }); 14 </script> 15 </head> 16 <body> 17 <h1 id="main">这是h1标题中的一些文本。</h1> 18 </body> 19 </html>
上面代码,我们给参数传入一个回调函数,将 DOMReady(封装函数) 方法封装为一个函数,方便以后使用,该方法支持所有现代浏览器,但是不支持 IE9 之前的浏览器。
4、HTML 嵌套规则
了解 HTML 嵌套规则,是进行 DOM 操作的基础。
HTML 存在许多种类型的标签,有的标签下面只允许特定的标签存在,这就叫 HTML 嵌套规则。
如果不按 HTML 嵌套规则写,浏览器就不会正确解析,会将不符合嵌套规则的节点放到目标节点的下面,或者变成纯文本。
所以在编写任何代码时,都需要按照规则编写,有利于解析,有利于操作,有利于优化,有利于维护,有利于重构。
HTML 元素可简单的分为块状元素和内联元素两类。下面是一些需要注意的嵌套规则:
①、块元素可以包含内联元素或某些块元素,但内联元素却不能包含块元素,他只能包含其他的内联元素,li元素内可以包含 div 元素。
<div> <h1></h1> <p></p> </div> <a href=""><span class=""></span></a> <ul> <li> <div></div> </li> </ul>
②、块元素不能包含在p元素内。
1 <p>这是一些文本 2 <div style="width:100px;height:200px;border:1px solid black;"></div> 3 </p> 4 5 <p>这是一些文本 6 <ul> 7 <li>苹果</li> 8 <li>香蕉</li> 9 </ul> 10 </p>
测试上面的代码,虽然他们都能被正常显示,但是并不处在同一层。
③、有几个特殊的块元素只能包含内联元素,不能包含块元素,这几个特殊的块元素是 h1-h6、p 和 dt。
④、块元素与块元素并列,内联元素与内联元素并列。
1 <div> 2 <h2></h2> 3 <p></p> 4 </div> 5 6 <div> 7 <a href=""></a> 8 <span class=""></span> 9 </div>
5、DOM 操作
DOM 节点是一个非常复杂的东西,对于他的每一个属性的访问,有可能会向上搜寻到 n 多个原型点,因此 DOM 操作是个很耗性能的操作。
使用 DOM 操作很复杂,所以建议尽量使用现成的框架来实现业务,比如 MVVM 框架,将所有的 DOM 操作,都转交给框架内部做精细有效的处理。