揭开DOM面纱
DOM(文档对象模型),Document Object Model,是HTML和XML文档的编程接口。DOM表示由多层的节点构成的文档,通过操作节点,开发者可以在页面上添加,删除,修改页面上的任何部分。
DOM早期脱胎于网景和微软的动态HTML,经过几十年的发展,如今的DOM实现了真正的跨平台,而且是网页的必要组成部分,也就是说一个网页离不开DOM的渲染支持,你想要实现一个网页,就需要使用DOM。现在前端领域比较火的框架有有vue,react,他们的底层也都离不开DOM操作,只是这些框架在DOM 的基础上,优化了一些性能,弥补了一些频繁操作DOM所带来的性能损失。如果你想更好的了解前端框架的底层原理的话,了解一些DOM知识是必不可少的。
节点层级:任何的HTML和XML文档都可以用DOM表示为一个由节点构成的层级结构。节点可以分为很多类型,每种类型都有每种类型自己独特的方法,可以把节点简单理解为蚂蚁这个种群,蚂蚁分为兵蚁,工蚁,蚁后等,它们分工不同,但却相互依赖,共同存在,是蚂蚁种群生存繁衍的根本。节点也是这样,在一个页面中我们需要的不只是一个节点。我们需要元素节点,属性节点,文本节点等一块渲染出一个丰富多彩,充满良好交互的页面。所以节点就被定义为了多种,以帮助我们实现更多有趣的东西。
Node类型:DOM中一共有12中节点类型,这些类型都继承一种基本类型。在DOM1(可以理解为一个DOM标准) 中描述了Node的接口,所有的DOM节点类型都必须实现这个接口。在js中,所有的类型都共享相同的基本属性和方法。每个节点都有nodeType属性,表示该节点的类型,节点类型由定义在Node类型上的12个数值常量表示:
- Node.ELEMENT_NODE (1)
- Node.ATTRIBUTE_NODE (2)
- Node.TEXT_NODE (3)
- Node.CDATA_SELECTION_NODE (4)
- Node.ENTITY_REFERENCE_NODE (5)
- Node.ENTITY_NODE (6)
- Node.PROCESSING_INSTRUCTION_NODE (7)
- Node.COMMENT_NODE (8)
- Node.DOCUMENT_NODE (9)
- Node.DOCUMENT_TYPE_NODE (10)
- Node.DOCUMENT_FRAGMENT_NODE (11)
- Node.NOTATION_NODE (12)
节点类型可以通过与这些常量比较来确定,例如:
if(someNode.nodeType == Node.ELEMENT_NODE){
alert("node is a element");
}
nodeName与nodeValue: 这两个值保存这有关节点的信息,而且这两个的值完全取决于节点的类型,在使用这两个属性前,最好先检测你的节点类型,然后再做相应的操作。例如:
if(someNOde == 1) {
value = someNode.nodeName; //保存了元素的标签名
}
对于元素节点而言,nodeName的值始终是元素的标签名,nodeValue是null。
节点关系: 文档中的所有节点都与其他节点有关系,这些关系可以形容为家族关系。子节点和父节点的关系。每一个节点都有一个childNodes属性,其中包含一个NodeList的实例,NodeList是一个类数组对象,它是有序的,可以按位置存取,但是它不是Array的实例,我们可以用[]这样的方式访问它,而且它也有length属性。NodeList对象独特的地方在于它是一个对DOM结构的查询,DOM结构的变化会自动的在NodeList中反映出来,所以我们可以理解为它是一个活动的,动态的对象,里面的存储的内容可以是实时的发生变化的。
下面是访问NodeList的用法:
let firstChild = someNode.childNodes[0];
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length;
NodeList是可以被转化为数组的,我们可以看看NodeList长什么样子,如下图:
将NodeList转为数组的方法如下:
let arrayNodes = Array.prototype.slice.call(someNode.childNodes,0);
let arrayNodes = Array.from(someNode.childNodes);
//每个节点都有一个parentNode属性,指向其DOM树中的父元素。childNodes中的所有节点都有一个共同的父元素,
//childNodes中的节点之间是兄弟节点,它们之间可以使用previousSibling和nextSibling进行导航,
if(someNode.nextSibling === null) {
alert('我是列表里的最后一个元素');
} else if(someNode.previousSibling === null) {
alert('我是列表里的第一个元素')
}
//注意 如果childNodes中只有一个节点,则它的previousSibling,nextSibling属性都是null
//父节点和它的第一个,最后一个节点也有专门的属性访问,firstChild,lastChild分别指向childNodes中的第一个和最后一个节点。
//someNode.firstChild的值始终等于someNode.childNodes[0],而someNode.lastChild的
//值始终等于someNode.childNodes[someNode.childNodes.length-1]
//如果只有一个子节点,firstChild,lastChild指向同一个节点,没有子节点的话,firstChild,lastChild都是null。
hasChildNodes()
判断一个节点是否有子节点还可以通过使用hasChildNodes(),这个方法如果返回是true的话,说明该节点有一个或者多个子节点。
ownerDocument属性
该属性是一个指向代表整个文档的文档节点的指针,文档中的所有节点都被创建它们的文档所拥有。如下图所示,同一个文档中的节点的ownerDocument属性是相同的。
操纵节点的方法
DOM中的所有关系指针都是只读的,如果你想插入节点或者删除节点的话只能使用DOM提供的方法。
appendChild()
用于在childNodes列表末尾添加新节点,添加新的节点会更新相关节点的关系指针,父节点会有一个指针指向新添加的节点上,新节点的父指针会指向父节点,之前的最后一个节点的下一个兄弟指针会指向新添加的节点上。 该方法会返回新添加的节点。
let returnedNode = someNode.appendChild(newNode);
alert(returnedNode == someNode.newNode);//true
alert(someNode.lastChild == newNode); //true
//注意,如果把文档中已经存在的节点传给appendChild(),则这个节点会从之前的位置被转移到新的位置上去
let returnedNode = someNode.appendChild(someNode.firstChild);
alert (returnedNode == someNode.firstChild)//false
alert(returnedNode == someNode.lastChild)//true
//如果想把节点放在特定的位置上去不是末尾的话,可以使用insertBefore()方法。
insertBefore()
该方法接受两个参数,要插入的节点和参照节点。调用这个方法后,要插入的节点会变成参照节点的前一个兄弟节点,并被返回。如果参照节点是null的话,则insertBefore与appendChild效果相同。
returnedNode = someNode.insertBefore(newNode,null);
alert(newNode == someNode.lastChild)//true
//作为新的第一个子节点插入
returnedNode = someNode.insertBefore(newNode,someNode.firstChild);
alert(returnedNode == newNode)//true
alert(newNode == someNode.firstChild)//true
//插入在最后一个子节点前面
returnedNode = someNode.insertBefore(newNode,someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.length-2]);//true
appendChild和insertBefore在插入节点时都不会删除任何已有的节点。
replaceChild()
该方法接受两个参数,要插入的节点和要替换的节点。要替换的节点会被返回并从文档树中完全移除,要插入的节点会取而代之。
removeChild()方法
移除某个节点,该方法接受一个参数,即要移除的节点,被移除的节点会被返回。
//replaceChild
let returnedNode = someNode.replaceChild(newNode,someNode.firstChild);
//removeChild
let formerFirstChild = someNode.removeChild(someNode.firstChild);
//注意,上面介绍的这四个操纵节点的方法都用于操纵某个节点的子元素,也就是说在使用他们之前必须先取得父节点。
cloneNode()
cloneNode()方法会返回与调用它的节点一模一样的的节点。该方法接受一个布尔值参数,表示是否是深复制,参数是true的时候会深复制,即复制该节点和它的子树,传false的时候,则只复制调用该方法的节点。复制返回的节点属于文档所有,但是还没有指定父节点,暂且称它为孤儿节点。如果要将它添加到文档中需要调用上面讲的方法给它指定一个父节点挂载到文档中。
未完待续...