引子

先来一颗栗子:

<img src="/sub/123.jpg" alt="test" />
<script type="text/javascript">
var img = document.getElementsByTagName('img')[0];
console.log('src:', img.src);
</script>

输出 src: sub/123.jpg?No,输出的是 src: http://127.0.0.1:8020/sub/123.jpg, 但我其实只想要一个pathname而已啊。虽然有一万种办法可以从完整地址中取出pathname,但我还是想一次获取咱们写到属性里面的那个src啊。

当然,这样的接口必须是有的:

console.log('src:', img.getAttribute('src'));
// src: /sub/123.jpg

以前总觉得HTML里面东西不多,但现在发现其实是自己了解的不多,还是too naive啊!DOM结构居然都这么不熟悉。找到问题,又回去看了看高程3,总觉得里面按照DOM123分类很没逻辑,还是试着按照功能区分总结一下吧。

DOM节点

先看看DOM结构层次,上个栗子:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM</title>
</head>
<body>
<!--wrapper-->
<div class="wrp">
wrp:
<img src="/sub/123.jpg" alt="test" />
<script type="text/javascript">
var img = document.getElementsByTagName('img')[0];
console.log('src:', img.getAttribute('src'));
</script>
</div>
</body>
</html>

这种比较常见的HTML文本,里面就已经包含了我们常用到的一个Node类型。DOM会将HTML文档以节点树的形式组织起来,也就是说Node与HTML文档是对应,即使是代码中的换行这种美观上东西,也会真实地反映到节点树上。

上面的HTML对应的Nodelist:

整个文档就是一个document节点,这个我们用的也比较多了。document中有几个属性用的比较少:URL、domain、referrer,其中domain可以设置,利用这一点可以解决一些子域之间的跨域问题。另外的一些特殊集合,如forms、images、links这些,可以方便操作,做爬虫的时候也可以用来简化模型。

document下面有document type、element、comment、text几种常用的节点。

这里要注意的是Text节点和element节点的区别,通常我们会在element节点里面写入字符,但并不是说element里面包含字符,而是element节点里面嵌入了一个text节点,text节点里面的内容才是我们写进的字符。例如 <p>papapa</p>的结构应该是:

> 换行

另外一个常被忽略的是换行。为了美化代码,通常我们每个标签之间都会换行,DOM在解析HTML文档时,会把换行也解析成 Textnode 节点!也就是我们每一行HTML代码后面都自带一个textnode节点\n!

> 节点关系

说到关系,也就是父子关系、兄弟关系这两种了。为了定位一个节点,我们可以需要DOM提供的几个定位接口:firstChild、lastChild、previousSibling、nextSibling,还有parentNode、childNodes。利用这几个接口的组合就可以定位到具体某个节点了。

还有一点就是Nodelist本身,Nodelist是一个动态的类数组对象,动态的意思就是DOM发生改变之后,变动会实时更新到Nodelist上,从这个性质来看,Nodelist应该是一个引用集或者指针集。类数组的意思就是说其实人家不是真正的数组,只是长得有点像。所以遍历的时候用for in也会将某些诸如对象本身的方法属性都遍历出来啦,还是常规的for i to length就好了,也可以用forEach、for of啦。

> 节点属性

每个node节点都有相应的nodetype、nodeName和nodeValue属性,见名知义,分别代表了节点的类型、名字和值。这里要说一下,element节点的nodeName为标签名,而nodeValue则为null,文本和comment的nodeValue为相应的字符串。

DOM操作

说到操作,无非就是增删改查。而DOM操作中,主体都是element节点,所以基本也是对element节点进行操作。

DOM的查有两种:准确查询和遍历查询。

1) 准确查询是给定一个条件去搜索,主要有两类接口:getElementBy***与querySelector***。

这两种查询的区别在于实时性与非实时性。getElement**的方式返回的是Nodelist,所以具有实时性。而querySelector**返回的是一个快照,DOM表的变化不能实时反映到查询的结果里面。看看这个例子:

<div class="wrp">
<p>papapa</p>
</div>
<script>
var divs = document.getElementsByClassName('wrp'), // 复制7个
// var divs = document.querySelectorAll('.wrp'), // 复制1个
i,
div = null;
for(i = 0; i < divs.length; i++) {
div = divs[0].cloneNode(true);
document.body.appendChild(div);
if(i > 5) {
break;
}
}
</script>

采用getElements***的方式,那divs是动态变化的,所以会复制7个papapa出来。假如采用querySelector***的方式,则divs只保存了刚查询的时候那个状态,因此只会复制一个papapa出来。所以,假如采用getElement的方式查询,那么就需要采用一个len变量来保存当前状态,否则就会造成死循环。按照我们平常的认知思维,divs也不应该动态变化,毕竟我后面没有继续查询啊,你怎么能变呢。所以,querySelector的方式更可控,不会搞出一些莫名其妙的bug,而且jQuery风格想必大家还是喜欢的。

2)遍历

DOM提供了2个遍历迭代器:NodeIterator与TreeWalker,当然你也可以手动实现遍历。

NodeIterator的用法看下面例子:

 <div class="wrp">
<p>papapa</p>
<div>
<span>yohohoho</span>
<input class='input1' type="text" name="" value="" />
</div>
<input class="input2" type="text" name="" value="" />
</div>
<script>
var div = document.querySelector('.wrp');
var filter = function(node) {
return node.tagName.toLowerCase() === 'input' ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
}
var myIterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
var n = myIterator.nextNode(); // move to root(div)
while(n !== null) {
console.log(n.className);
n = myIterator.nextNode();
}
</script>

TreeWalker遍历:

 var div = document.querySelector('.wrp');
var filter = function(node) {
return node.tagName.toLowerCase() === 'input' ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
} var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false);
var n = walker.firstChild();
while(n !== null) {
console.log(n.className);
n = walker.nextNode();
}

手动方式,先说说思路:首先要考虑子级中还有子级,那显然这里要用递归的形式来做,判断节点是否有子节点而递归。然后就是一些细节方面的,比如为了剔除换行符和文本节点的影响,那当然是直接使用children来获取子级节点了。另外还需要避免Nodelist的动态变化之类的。看代码:

 function mywalker(el, filter, cb) {
var children = el.children;
for(var i = 0, len = children.length; i < len; i++) {
var walker = children[i];
if(walker.children.length !== 0) {
mywalker(walker, filter, cb);
} else if(filter(walker)) {
cb(walker);
}
}
} var div = document.querySelector('.wrp'); function filter(node) {
return node.tagName.toLowerCase() === 'input';
} function cb(node) {
console.log(node.className);
} mywalker(div, filter, cb);

如果还要考虑文本节点的话,那可以使用childNodes来遍历,这个时候就要注意过滤换行符了。

DOM节点的增接口有:appendChild() 和 insertBefore()。但创建节点则需要用到另外两个接口:document.creat***方式和document.cloneNode()。创建节点之后通过添加接口将节点放入文档中。这里除了可以创建普通的节点之外,也可以创建脚本和样式表,这种用法可以实现按需加载,延迟加载等各种资源加载方法。当然要做成像requirejs那样的加载器,那就需要添加很多处理逻辑了。

删除有两个接口:replaceChild、removeChild。

1)内容的更改:innerHTML、innerText,textContent之类的。textContent一般不会用到,他是将节点内部所有文本节点拼接在一起的字符串,包括换行符这些也都塞了进去,所以还需要进一步进行字符串处理。

2)属性的更改

文章开头其实就已经提到了最常用的两种更改属性的方法,分别是 Element.props=value 和 Element.setAttribute() 系列。其中getAttribute()取的是Attribute节点上的值,也就是对HTML标签的尖括号<>内的这串字符进行查询。假如没定义则返回一个null,有定义的则返回一个字符串。而Element.props是对HTML标签的实例进行查询,也就是对一个实例化后的节点对象进行查询。而这个对象在初始化的时候就会将HTML标签中没定义的属性置为””有定义则进行解析转换。所以对同一个对象进行查询,没定义的属性.prop返回空值而getAttribute返回null;对style和onload一类事件属性进行查询,前者返回一个对象,而后者返回一个字符串或null;对自定义属性的查询,前者返回一个undefined,而后者则返回自定义的值。

而文章开头中两种查询得到的结果不同,原因就在于此。

attributes

节点中统一管理各种属性的是 attributes 属性,它是一个NamedNodeMap,保存了对象已定义的所有属性。这货其实也是一个类数组,这里chrome倒是打印的很清楚,一目了然。记得不能用for in遍历哦。

可以通过attributes[‘id’]的形式去查询相应的属性,它自身也有一些方法,but没什么人会用它,而且attributes这个属性本身就很少会被用到。使用它的场景之一(或者可以说唯一)就是要对属性的集中管理,或者遍历或者批量初始化,而平常我们用的基本都是.prop的形式,直观方便。

> 自定义属性

我们可以往标签里加入任意的自定义属性,自由虽好但规范还是要有。HTML5中是以data-prop的形式来定义自定义属性的。挂在data下的属性就可以通过dataset属性统一管理,一般可以将数据放到data上,然后渲染的时候直接读取dataset属性进行渲染了,像knockout这种库也是这么来进行数据绑定的。

> 样式

属性中非常重要的一点就是样式,这里又涉及到内联样式和嵌入、外联两类的样式控制。上文也说到,使用element.stylt.样式可以直接更改样式,也可以通过element.style.cssText来一次读写所有的内联样式。但样式比较复杂的地方是嵌入、外联样式的叠加影响,所以单纯查询style并不能得到元素的最终样式。这个时候可以通过document.defaultView的getComputedStyle()方法来获取计算样式,就像chrome开发者工具那样得到计算样式。

但是,CSS是个大工程,试图通过js来动态控制所有样式的变化是不靠谱的。更为常见的做法是CSS预设各种状态,而js作为控制来控制状态的转换,也就是控制class的变换。DOM编程中除了单纯的className之外,更为强大的接口是classList,这又是一个类数组对象。它提供了几个好用的类管理方法:add、contains、remove、toggle,满满jQuery风啊。但jQuery毕竟是个民间女子,当正统后宫吸收了她的各种奇法淫技之后,失宠也是不可避免的。

小结

琢磨了好久,但写出来还是各种凌乱,结构还是不够清晰。DOM的结构层次以Nodelist为基础,不同nodeType的节点有机组合就构成了一个DOM表。对DOM的操作有各种接口,感觉比较混乱,可能这也是各种框架一直致力于弱化开发者对DOM操作的原因吧,但在超小规模应用上熟悉了DOM操作的话,还是会比使用框架更加灵活。

对DOM的总结就到这里了,看了下还是整理为主,背书为辅哈,必须填图了。

说说DOM的那些事儿的更多相关文章

  1. React virtual DOM explained in simple English/简单语言解释React的虚拟DOM

    初学React,其中一个很重要的概念是虚拟DOM,看了一篇文章,顺带翻译一下. If you are using React or learning React, you must have hear ...

  2. DOM LEVEL 1 中的那些事儿[总结篇-下]

    本文承接:DOM LEVEL 1 中的那些事儿[上]   2.3 Element类型 Element类型应该是Document类型之外使用的最多的节点类型了,Element代表XML或HTML文档中的 ...

  3. dom那些事儿

    一.dom常识1.style属性style对象的属性值都是字符串,设置时必须包括单位,但是不含规则结尾的分号.比如,elem.style.width不能写为100,而要写为100px. 2.getCo ...

  4. DOM LEVEL 1 中的那些事儿[总结篇-上]

    DOM是前端编程中一个非常重要的部分,我们在动态修改页面的样式.内容.添加页面动画以及为页面元素绑定事件时,本质都是在操作DOM.DOM并不是JS语言的一个部分,我们通过JAVA.PHP等语言抓取网页 ...

  5. 什么是jquery $ jQuery对象和DOM对象 和一些选择器

    1什么是jQuery: jQuery就是将一些方法封装在一个js文件中.就是个js库 我们学习这些方法. 2为什么要学习jQuery: 原生js有以下问题: 1.兼容性问题2.代码重复3.DOM提供的 ...

  6. 从click事件理解DOM事件流

    事件流是用来解释页面上的不同元素接受一个事件的顺序,首先要明确两点: 1.一个事件的影响元素可能不止一个(同心圆理论),但目标元素只有一个. 2.如果这些元素都绑定了相同名称的事件函数,我们怎么知道这 ...

  7. AngularJS in Action读书笔记2——view和controller的那些事儿

    今天我们来818<angularjs in action>的第三章controller和view. 1.Big Picture概览图 View是angularjs编译html后呈现出来的, ...

  8. Hybrid框架UI重构之路:六、前端那点事儿(Javascript)

    上文回顾 :Hybird框架UI重构之路:五.前端那点事儿(HTML.CSS) 这里讲述在开发的过程中,一些JS的关键点. 换肤 对于终端的换肤,我之前一篇文章有说了我的想法. 请查看:http:// ...

  9. 对比DOM和jQuery完善度

    <input type="text" id="username" value="请输入你的用户名"> <script> ...

随机推荐

  1. Android UI体验之全屏沉浸式透明状态栏效果

    前言: Android 4.4之后谷歌提供了沉浸式全屏体验, 在沉浸式全屏模式下, 状态栏. 虚拟按键动态隐藏, 应用可以使用完整的屏幕空间, 按照 Google 的说法, 给用户一种 身临其境 的体 ...

  2. 闲来无聊,研究一下Web服务器 的源程序

    web服务器是如何工作的 1989年的夏天,蒂姆.博纳斯-李开发了世界上第一个web服务器和web客户机.这个浏览器程序是一个简单的电话号码查询软件.最初的web服务器程序就是一个利用浏览器和web服 ...

  3. Linux 开机时网络自动连接

      简单版本: cd /etc/sysconfig/network-scripts/ vi ifcfg-enoXXX 输入:reboot重启 或者输入:service network restart ...

  4. Mysql事务探索及其在Django中的实践(一)

    前言 很早就有想开始写博客的想法,一方面是对自己近期所学知识的一些总结.沉淀,方便以后对过去的知识进行梳理.追溯,一方面也希望能通过博客来认识更多相同技术圈的朋友.所幸近期通过了博客园的申请,那么今天 ...

  5. System.FormatException: GUID 应包含带 4 个短划线的 32 位数(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。

    在NHibernate数据库查询中出现了这个错误,由于是数据库是mysql的,当定义的字段为char(36)的时候就会出现这个错误. [解决方法] 将char(36) 改成varchar(40)就行了 ...

  6. [原] KVM 虚拟化原理探究(6)— 块设备IO虚拟化

    KVM 虚拟化原理探究(6)- 块设备IO虚拟化 标签(空格分隔): KVM [toc] 块设备IO虚拟化简介 上一篇文章讲到了网络IO虚拟化,作为另外一个重要的虚拟化资源,块设备IO的虚拟化也是同样 ...

  7. javascript排序

    利用array中的sort()排序 w3cfunction sortNumber(a,b) { return a - b } var arr = new Array(6) arr[0] = " ...

  8. 信息安全-1:python之playfair密码算法详解[原创]

    转发注明出处: http://www.cnblogs.com/0zcl/p/6105825.html 一.基本概念 古典密码是基于字符替换的密码.加密技术有:Caesar(恺撒)密码.Vigenere ...

  9. docker4dotnet #2 容器化主机

    .NET 猿自从认识了小鲸鱼,感觉功力大增.上篇<docker4dotnet #1 前世今生&世界你好>中给大家介绍了如何在Windows上面配置Docker for Window ...

  10. linux基础命令

    系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMBIOS ...