• 说说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

  • 相关阅读:
    53. Maximum Subarray
    64. Minimum Path Sum
    28. Implement strStr()
    26. Remove Duplicates from Sorted Array
    21. Merge Two Sorted Lists
    14. Longest Common Prefix
    7. Reverse Integer
    412. Fizz Buzz
    linux_修改域名(centos)
    linux_redis常用数据类型操作
  • 原文地址:https://www.cnblogs.com/qieguo/p/5679330.html
Copyright © 2020-2023  润新知