引子

先来一颗栗子:

<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. node-webkit 环境搭建与基础demo

    首先去github上面下载(地址),具体更具自己的系统,我的是windows,这里只给出windows的做法 下载windows x64版本 下载之后解压,得到以下东西 为了方便,我们直接在这个目录中 ...

  2. Azkaban源码学习笔记

    1. ConnectorParams (interface): 定义了各种常量参数,没有声明任何方法. 2. ExecutorServlet.java类   2.1 继承类HttpServlet和接口 ...

  3. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  4. 【Python五篇慢慢弹】快速上手学python

    快速上手学python 作者:白宁超 2016年10月4日19:59:39 摘要:python语言俨然不算新技术,七八年前甚至更早已有很多人研习,只是没有现在流行罢了.之所以当下如此盛行,我想肯定是多 ...

  5. Velocity笔记--使用Velocity获取动态Web项目名的问题

    以前使用jsp开发的时候,可以通过request很轻松的获取到根项目名,现在换到使用velocity渲染视图,因为已经不依赖servlet,request等一些类的环境,而Web项目的根项目名又不是写 ...

  6. 玩转Vim 编辑器

    一:VIM快速入门 1.vim模式介绍 以下介绍内容来自维基百科Vim 从vi演生出来的Vim具有多种模式,这种独特的设计容易使初学者产生混淆.几乎所有的编辑器都会有插入和执行命令两种模式,并且大多数 ...

  7. ASP.NET Core 中文文档 第四章 MVC(4.3)过滤器

    原文:Filters 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:何镇汐 ASP.NET MVC 过滤器 可在执行管道的前后特定阶段执行代码.过滤器可以配置为全局有效.仅对控 ...

  8. 代码的坏味道(15)——冗余类(Lazy Class)

    坏味道--冗余类(Lazy Class) 特征 理解和维护类总是费时费力的.如果一个类不值得你花费精力,它就应该被删除. 问题原因 也许一个类的初始设计是一个功能完全的类,然而随着代码的变迁,变得没什 ...

  9. 【转】为什么我们都理解错了HTTP中GET与POST的区别

    GET和POST是HTTP请求的两种基本方法,要说它们的区别,接触过WEB开发的人都能说出一二. 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数. 你可能自己 ...

  10. Unable to create the selected property page. An error occurred while automatically activating bundle net.sourceforge.pmd

    解决方案: 在命令行到eclipse目录下使用 eclipse.exe -clean