说说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:
整个文档就是一个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的用法看下面例子:
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遍历哦。
可以通过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的总结就到这里了,看了下还是整理为主,背书为辅哈,必须填图了。