• 说说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节点 !

    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

  • 相关阅读:
    【Leetcode】113Path Sum II
    【leetcode】112. Path Sum
    virtualbox 中安装win7虚拟机
    制作一个vagrant的win7 box
    socket编程
    异常处理
    strip(),replace()和re.sub()用法
    面象对象 高阶篇
    面象对象 基础篇
    Subprocess模块介绍
  • 原文地址:https://www.cnblogs.com/qieguo/p/5679330.html
Copyright © 2020-2023  润新知