Fork me on GitHub

说说DOM的那些事儿

引子

先来一颗栗子:

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

20160717104556

整个文档就是一个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>的结构应该是:

20160717110015

> 换行

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

20160717181742

> 节点关系

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

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

20160717183150

> 节点属性

每个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的用法看下面例子:

  1 <div class="wrp">
  2     <p>papapa</p>
  3     <div>
  4         <span>yohohoho</span>
  5         <input class='input1' type="text" name="" value="" />
  6     </div>
  7     <input class="input2" type="text" name="" value="" />
  8 </div>
  9 <script>
 10     var div = document.querySelector('.wrp');
 11     var filter = function(node) {
 12         return node.tagName.toLowerCase() === 'input' ?
 13             NodeFilter.FILTER_ACCEPT :
 14             NodeFilter.FILTER_SKIP;
 15     }
 16     var myIterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
 17     var n = myIterator.nextNode(); // move to root(div)
 18     while(n !== null) {
 19         console.log(n.className);
 20         n = myIterator.nextNode();
 21     }
 22 </script>

TreeWalker遍历:

  1 var div = document.querySelector('.wrp');
  2 var filter = function(node) {
  3     return node.tagName.toLowerCase() === 'input' ?
  4         NodeFilter.FILTER_ACCEPT :
  5         NodeFilter.FILTER_SKIP;
  6 }
  7 
  8 var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, filter, false);
  9 var n = walker.firstChild();
 10 while(n !== null) {
 11     console.log(n.className);
 12     n = walker.nextNode();
 13 }

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

  1 function mywalker(el, filter, cb) {
  2     var children = el.children;
  3     for(var i = 0, len = children.length; i < len; i++) {
  4         var walker = children[i];
  5         if(walker.children.length !== 0) {
  6             mywalker(walker, filter, cb);
  7         } else if(filter(walker)) {
  8             cb(walker);
  9         }
 10     }
 11 }
 12 
 13 var div = document.querySelector('.wrp');
 14 
 15 function filter(node) {
 16     return node.tagName.toLowerCase() === 'input';
 17 }
 18 
 19 function cb(node) {
 20     console.log(node.className);
 21 }
 22 
 23 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遍历哦。

20160717183545

可以通过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的总结就到这里了,看了下还是整理为主,背书为辅哈,必须填图了。

053

posted @ 2016-07-18 00:42  茄果  阅读(1117)  评论(2编辑  收藏  举报