Node
是一个接口,各种类型的DOM API对象都会从这个接口继承;有继承就有原型
先看一下都有哪些DOM
查找一下原型链
function getParents(el) {
if(typeof el !== 'object' || el === null) {
throw new Error("el应该是一个对象")
}
let _el = el;
const res = [];
while (_el.__proto__ !== null) {
res.push(_el.__proto__.constructor.name);
_el = _el.__proto__
}
return res;
}
可以看到元素节点以及document都是继承自Node,并且都来自EventTarget
NodeType
用于区分节点的类型,一共有12种,下面列出常用的几类
元素节点
NodeType为1,比如span、div
创建方式: document.createElement
属性:
1、children(返回nodeType=1的节点):自己的属性
2、childNodes(返回所有内容):Node的属性,childNodes中包含注释和空白节点
<div id="root">
啊哈嗯哼
<!-- 这是注释啊 -->
<span>哦喝</span>
</div>
3、使用getAttribute获取属性
4、使用setAttribute设置属性
文本
NodeType为3,对象模型是Text
注意:
空白的文本节点(换行或者空格引起),空白的文本节点也是可以被获取的
需要使用childNodes访问
使用nodeValue取值
<div id="root">啊哈<span>嗯哼</span></div>
<div id="root">
啊哈
<span>嗯哼</span>
</div>
API
splitText:拆分文本
Element.normalize:合并文本
<body>
<div id="root">
啊哈嗯哼
</div>
<button id="btnSplit">拆分</button>
<button id="btnNormalize">合并</button>
</body>
<script>
const divE = document.getElementById("root");
btnSplit.addEventListener("click",function() {
divE.firstChild.splitText(5); // 参数必须传入数字,传入任何数字都一样
console.log(divE.childNodes.length) // 2
})
btnNormalize.addEventListener("click",function() {
divE.normalize();
console.log(divE.childNodes.length) // 1
})
</script>
注释
NodeType为8,对象模型是Comment;使用nodeValue取值;如果有注释childNodes中是包含的
<body>
<div id="root">
啊哈嗯哼
</div>
</body>
<script>
const cEl = new Comment("这是注释啊");
root.appendChild(cEl);
console.log(cEl.nodeValue) // 这是注释啊
</script>
文档
NodeType为9,对象模型是Document
方法和属性
节点查找: document.querSelector、document.querySelectorAll;传入css选择器
节点结合信息: document.all、document.froms、document.scripts、document.images、document.links
cookie: document.cookie
使用document解析XML
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(`
<xml>
<persons>
<person>
<name>小明</name>
<age>18</age>
</person>
<person>
<name>小红</name>
<age>18</age>
</person>
</persons>
</xml>
`,"text/xml")
console.log(xmlDoc.__proto__.constructor)
// ƒ XMLDocument() { [native code] }
const persons = xmlDoc.querySelectorAll("person");
const personJSON = Array.from(persons).map((node)=>{
return {
name: node.querySelector("name").childNodes[0].nodeValue,
age: node.querySelector("age").childNodes[0].nodeValue
}
})
console.log(personJSON)
文档类型
NodeType为10;对象模型是DocumentType;访问方式:document.doctype、document.firstChild;有用的属性只有name,返回"html"
如果头部有注释,firstChild返回的是注释
文档碎片
NodeType为11;对象模型是DocumentFragment;就和标准的document一样,存储由节点组成的文档结构所有的节点会被一次插入到文档中,这个操作仅发生一次重绘
手动创建节点
<script>
// 创建注释
const commentEl = new Comment("标题");
document.body.appendChild(commentEl);
// 创建div
const divEl = document.createElement("div");
// 设置属性
divEl.setAttribute("title", "div的title")
// 设置样式
divEl.style.backgroundColor = "red";
// 和textContent等价
divEl.appendChild(new Text("JavaScript"))
const spanEl = document.createElement("span");
spanEl.textContent = "进阶";
divEl.appendChild(spanEl);
document.body.appendChild(divEl)
</script>
元素的查找
Node和Element
Node是一个接口,我们一般叫节点;所有Node子类的集合称为NodeList(节点列表),由childNodes返回
Element是通用性的基类,nodeType值为1,是Node的一类实现;其子类我们统称为元素;Node还有很多的其他实现,比如文本、注释;Element子类的集合叫HTMLCollection(元素集合),由children返回
getElementById
根据元素的id属性值进行节点查询,返回单一元素,属于高效查询;
只返回nodeType为1的element;
id是大小写敏感的字符串
如果有多个相同id的元素,返回第一个元素
此方法仅存在Document实例上,常用于document上
getElementsByClassName
指定的类名查询元素
语法:
document.getElementsByClassName(names)或者rootElement.getElementsByClassName(names)
注意事项:
1、返回结果是实时元素集合,但不是数组
2、可以同时匹配多个class,class类通过空格分割,是and关系
3、元素均有此方法,不限于document
<ul id="outer">
<li class="item item2">元素1</li>
<li class="item">元素2</li>
<li class="item">元素3</li>
</ul>
console.log(document.getElementById("outer").getElementsByClassName("item"))
console.log(document.getElementsByClassName("item item2"))
getElementsByname
根据指定的name属性查询元素
注意事项:
1、返回的是节点集合(NodeList)不是元素集合
2、包括不能被解析的节点,比如
3、此方法仅存在Document实例上
getElementsByTagName(tagName)
根据指定的标签查询元素
注意事项:
1、返回的是实时的元素集合
2、tagName可以是*,代表全部元素
3、webkit旧版本可能返回的是NodeList集合
4、不能被解析的节点也是可以查询的
querySelector
根据css选择器进行节点查询,返回匹配的第一个元素
注意事项:
1、如果传入的不是有效css选择器字符串,会抛出异常
2、仅返回第一个匹配的元素
3、元素也有此方法,不限于document
4、css选择器字符串的转义字符;必须将它两次转义,一次是Javascript字符串转义,一次是为querySelector转义
querySelectorAll
根据css选择器进行节点查询,返回节点列表的NodeList
注意事项:
1、返回的是静态的NodeList,之后对DOM元素的改动不会影响其集合的内容
2、返回的可能不是你期望的值
3、元素也有此方法,不限于document
在选择子元素的时候需要使用:scope不然选择不准确
<div class="outer">
<div class="select">
<div class="inner"></div>
</div>
</div>
const select = document.querySelector(".select");
const inner = select.querySelectorAll(".outer .inner");
console.log(inner)
const inner2 = select.querySelectorAll(":scope .outer .inner");
console.log(console.log(inner2))
选择的是静态元素
<ul id="outer">
<li class="item item2">元素1</li>
<li class="item">元素2</li>
<li class="item">元素3</li>
</ul>
const nodeList = document.querySelectorAll(".item");
console.log(nodeList.length) // 3
// 添加新的元素
const liEl = document.createElement("li");
liEl.className = "item";
liEl.innerHTML = "元素4"
document.querySelector("#outer").appendChild(liEl);
console.log(nodeList.length) // 3
const nodeList = document.getElementsByClassName("item");
console.log(nodeList.length) // 3
// 添加新的元素
const liEl = document.createElement("li");
liEl.className = "item";
liEl.innerHTML = "元素4"
document.querySelector("#outer").appendChild(liEl);
console.log(nodeList.length) // 4
总结
特殊的查询
document.all:返回所有的元素
document.images:返回所有的图片元素
document.forms:返回所有的form表单元素
document.scripts:所有的脚本元素
document.links:所有具有href的area和a元素
document.fonts:所有的字体
document.styleSheets:所有css的link和style元素,区别是是否有href属性
伪元素
不能被获取,但是可以获取里面的值
<body>
<div id="outer">Jhon</div>
</body>
<style>
#outer::before {
content: "你好,";
font-size: 24px;
}
</style>
const before = window.getComputedStyle(outer,"before")["content"];
console.log(before) // 你好,
const before = window.getComputedStyle(outer,"before")["font-size"];
console.log(before) // 24px
元素的遍历
元素集合和NodeList遍历
虽然元素集合和NodeList不是数组,但是都有length属性,可以通过for或者while遍历
NodeList原型上有forEach:NodeList.prototype.forEach
转换成数组再遍历:Array.from、Array.prototype.slice、拓展运算符
某个节点、元素所有子节点、元素遍历
1、找到children或者childNodes
2、NodeIterator或者TreeWalker;TreeWalker是NodeIterator的一个更高级的版本;TreeWalk额外支持一些方法:parentNode、firstChild、lastChild、nextSibling;他们是深度遍历,会返回满足条件的节点
<ul id="outer">
<li class="item">元素1</li>
<li class="item">元素2</li>
<li class="item">元素3</li>
<li class="item">元素4</li>
</ul>
<script>
const iterator = document.createNodeIterator(
document.getElementById("outer"),
NodeFilter.SHOW_ELEMENT,
{
acceptNode(node){
return node.tagName === "LI" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
}
)
let currentNode;
while (currentNode = iterator.nextNode()) {
console.log(currentNode.innerText);
}
</script>
元素新增、删除、复制
新增
节点的创建和挂载,创建了节点未加入文档是没有任何视觉效果的;所以新增节点得两部:创建和挂载
显示创建:
1、对象模型直接new
const commentNode = new Comment("这是注释");
const text = new Text("这是文本");
document.body.append(commentNode)
document.body.append(text)
2、document.create系列
const el = document.createElement('div');
const commentNode = document.createComment("这是注释");
const textNode = document.createTextNode("这是文本");
const fragment = document.createDocumentFragment();
fragment.append("fragment创建的节点");
el.appendChild(textNode);
document.body.append(el);
document.body.append(commentNode);
document.body.append(fragment);
节点挂载
Node API挂载
<div>
<button id="btnAppendChild">appendChild</button>
<button id="btnInsertBefore">insertBefore</button>
<button id="btnReplaceChild">replaceChild</button>
<button id="btnTextContent">textContent</button>
</div>
<div>
<div id="buttons">
<button id="btnBase">基准按钮</button>
</div>
<div id="contents">
<span>前面的节点</span>
文本
<span>span的文本</span>
</div>
</div>
appendChild,将一个节点附加到指定父节点的子节点列表的末尾;语法:parentNode.appendChild()
btnAppendChild.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "appendChild";
buttons.appendChild(btn)
})
insertBefore,在参考节点之前插入一个拥有指定父节点的子节点;语法:parentNode.insertBefore(newNode,referenceNode);
btnInsertBefore.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "insertBefore";
buttons.insertBefore(btn,btnBase)
})
replaceChild,指定的节点替换当前节点的一个子节点,并返回被替换掉的节点;语法:parentNode.insertBefore(newNode,oldChild)
btnReplaceChild.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "replaceChild";
buttons.replaceChild(btn,btnBase)
})
textContent,替换一个节点及其后代的文本内容
btnTextContent.addEventListener("click",function(e) {
contents.textContent = "textContent";
})
Element API挂载;继承于Node API
<div>
<button id="btnAfter">after</button>
<button id="btnBefore">before</button>
<button id="btnAppend">append</button>
<button id="btnPrepend">prepend</button>
</div>
<div>
<div id="buttons">
<button id="btnBase">基准按钮</button>
</div>
</div>
after,在该节点之后插入一组Node
btnAfter.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "after";
// Element.affer(...nodesOrDOMStrings)
btnBase.after(btn,'after text')
})
before,在该节点之前插入一组Node;before和after操作后都是多了兄弟节点
btnBefore.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "before";
btnBase.before(btn,'before text')
})
append,在节点最后一个子节点后插入一组Node
btnAppend.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "append";
buttons.append(btn,'append text')
})
prepend,在节点的第一个子节点之前插入一组Node,append和prepend插入的都是子节点
btnPrepend.addEventListener("click",function(e) {
const btn = document.createElement("button");
btn.textContent = "prepend";
buttons.prepend(btn,'prepend text')
})
insertAdjacentElement,将节点插入给定的一个位置
参照节点是自身,要求传入的是元素,并且只能传入一个节点
<div>
<div class="parent">
parent
<div class="parent">child</div>
</div>
</div>
function createDiv(content) {
const el = document.createElement("div");
el.innerHTML = content;
return el;
}
const pEl = document.querySelector(".parent");
pEl.insertAdjacentElement("afterbegin",createDiv("afterbegin"));
pEl.insertAdjacentElement("afterend",createDiv("afterend"));
pEl.insertAdjacentElement("beforebegin",createDiv("beforebegin"));
pEl.insertAdjacentElement("beforeend",createDiv("beforeend"));
insertAdjacentHTML,将文本解析为节点并插入到给定的位置;参照节点是自身,要求传入的是字符串,语法和insertAdjacentElement一致
insertAdjacentText,将文本节点插入到给定的位置;参照节点是自身,要求传入的是文本字符串,语法和insertAdjacentElement一致
replaceChildren,将后代替换为指定节点
replaceWidth,将后代替换为指定节点集合
对比
innerHTML和innerText
innerHTML:批量创建并生成节点
innerText:生成文本节点,本质是HTMLElement上的属性
删除
删除单个节点: Node.removeNode、Element.remove、outerHTML或者innerHTML、Document.adoptNode
批量删除节点: outerHTML或者innerHTML、replaceChildren和replaceWidth
循环删除
function clearChildNodes(node){
while(node.hsaChildNodes()){
node.removeChild(node.firstChild)
}
}
复制节点
Node.cloneNode: 克隆节点,分为浅克隆和深克隆,传入tree会clone所有的子节点,false只会clone节点本身,会复制所有的属性以及属性值,如果使用onclick绑定的事件也会一起clone,但是不会拷贝使用addEventListener动态绑定的事件
Document.importNode: 从外部文档引入节点,可引入节点的后代节点,源节点被保留
Document.adoptNode: 从其他文档获取节点,节点和后代节点会从源节点删除
Element.innerHTML
内存泄漏
1、全局作用域;el被挂载到全部对象上是不会被回收的,想要回收需要设置el = null
2、console.log也会导致节点不被回收
<body>
<div id="root"></div>
</body>
<script>
var root = document.getElementById("root");
var el = document.createElement("div");
el.appendChild(new Text("这是文本"));
root.appendChild(el);
const wkRef = new WeakRef(el);
root.removeChild(el);
setInterval(() => {
if(wkRef.deref()){
console.log("el未被回收")
}else {
console.log("el已经回收")
}
}, 2000);
</script>
3、eval会将他的执行上下文中的内容全部保存,所以要小心使用eval
(function() {
var root = document.getElementById("root");
var el = document.createElement("div");
el.appendChild(new Text("这是文本"));
root.appendChild(el);
const wkRef = new WeakRef(el);
root.removeChild(el);
setInterval(() => {
if(wkRef.deref()){
console.log("el未被回收")
}else {
console.log("el已经回收")
}
}, 2000);
})()
使用IIFE函数,var将不会被挂载到全局作用域,一段时间后会被回收
使用eval之后将不会被回收
(function() {
var root = document.getElementById("root");
var el = document.createElement("div");
el.appendChild(new Text("这是文本"));
root.appendChild(el);
const wkRef = new WeakRef(el);
root.removeChild(el);
setInterval(() => {
eval("")
if(wkRef.deref()){
console.log("el未被回收")
}else {
console.log("el已经回收")
}
}, 2000);
})()
节点近亲属性对比
HTMLElement.innerText:表示一个节点以及后代被渲染的文本内容,强调被渲染
Node.textContent:表示一个节点以及其后代节点的文本内容
区别:innerText可操作已经被渲染的内容,textContent不会;innerText受样式影响会触发浏览器绘制部分或全部页面,带来性能问题
Node.nodeValue:对于text、comment和CDATA节点来说,nodeValue返回该节点的文本内容;对于attribute节点来说,返回该属性的属性值
value:特定的一些HTMLElement元素,用value属性来获取其值,一般使用于form表单元素
clientWidth、offsetWidth、scrollWidth
Element.clientWidth(元素宽度),包含width+左右padding(不包含滚动条、border、margin、滚动条)
HTMLElement.offsetWidth(元素布局宽度),包含width+左右padding+左右border+滚动条(不包含margin)
Element.scrollWidth(元素内容宽度),用于测量元素内容宽度,包含由于overflow溢出而在屏幕上不可见的内容
Node.comperDocumentPosition:比较当前节点与任意文档中的另一个节点的位置,返回数字,带组合意义的数据,不仅可以返回包含信息还能返回在之前或者之后等信息;语法:compareMask = node.compareDocumentPosition(outerNode)
Node.contains:返回的是一个布尔值,来表示传入的节点是否为该节点的后代节点,仅能返回包含信息
Element.getBoundingClientRect:返回元素的大小以及相对于可视化窗口的位置
Element.getClientRects:返回盒子的边界矩形集合;对于行内元素,元素内部的每一行都会有一个边框,对于块级元素,如果里面没有其他元素,一整块元素只有一个边框
window.onload:在文档装载完成之后会触发load事件,此时在文档中的所有对象都在DOM中,包括图片、脚本、链接、子框架等都是可以访问的
window.DOMContentLoaded:当初始的HTML文档被完全加载和解析完成之后事件将会被触发,无需等待样式表、图片、iframe等的完全加载,jq的$(function(){})
web component
vue scoped css实现的原理在元素标签上添加一个属性,一般以data-v开头,后面是随机数,然后使用属性选择器添加样式
自定义节点
<my-text></my-text>
<script>
class MyText extends HTMLElement {
constructor() {
super();
this.append("我的文本")
}
}
window.customElements.define("my-text",MyText)
</script>
<my-text></my-text>
放在注册后面就不会生效,可以把对节点的处理放到生命周期connectedCallback中
<my-text></my-text>
<script>
class MyText extends HTMLElement {
constructor() {
super();
}
connectedCallback(){
this.append("我的文本")
}
}
window.customElements.define("my-text",MyText)
</script>
<my-text></my-text>
三项主要技术
Custom elements(自定义元素)
自主定制元素:是独立的元素,不继承其他内建的HTML元素,例如上面的<my-text>
自定义内置元素:继承自基础的HTML元素,指定所需扩展的元素,要通过is属性指定custom element的名称,且名称必须包含一个短横线
<p color="red" is="color-p"></p>
<script>
class ColorP extends HTMLParagraphElement {
constructor() {
super();
this.style.color = this.getAttribute("color");
}
}
window.customElements.define("color-p",ColorP,{extends:"p"})
</script>
生命周期
connectedCallback:插入文档时;对节点的操作应位于此周期,可能被多次触发,比如删除后又添加到文档
disconnectedCallback:从文档删除时
adoptedCallback:被移动到新文档时
attributeChangeCallback:属性变化时;需要配合observeredAttributess属性一起使用,指定监听的属性,使用setAttribute方法更新属性
<div id="container1">
<p is="my-text" text="大家好" id="myText"></p>
</div>
<div id="container2"></div>
<div>
<button id="btnUpdateText">更新属性</button>
<button id="btnRemove">删除节点</button>
<button id="btnRestore">恢复节点</button>
</div>
<script>
class MyText extends HTMLParagraphElement {
constructor() {
super();
}
connectedCallback(){
console.log("connectedCallback");
this.append("触发了connectedCallback生命周期")
}
disconnectedCallback(){
this.innerHTML="";
console.log("disconnectedCallback")
}
static get observedAttributes(){
return ['text']
}
attributeChangedCallback(name,oldValue,newValue){
console.log("attributeChangedCallback:",name,oldValue,newValue)
}
adoptedCallback(){
console.log("adoptedCallback")
}
}
window.customElements.define("my-text",MyText,{extends:'p'})
const myTextEl = document.getElementById("myText");
btnUpdateText.addEventListener("click",function (e) {
myTextEl.setAttribute('text',"改变了text属性")
})
btnRemove.addEventListener("click",function (e) {
myTextEl.remove()
})
btnRestore.addEventListener("click",function (e) {
container1.appendChild(myTextEl)
})
</script>
<div id="container"></div>
<button id="btnAdopt">adoptNode</button>
<iframe src="./index.html" frameborder="0" id="ifr"></iframe>
<script>
btnAdopt.addEventListener("click", function(){
const textNode = ifr.contentWindow.document.getElementById("myText")
container.appendChild(document.adoptNode(textNode));
});
</script>
首次刷新触发
更新属性触发
删除节点触发
恢复节点触发
节点创建比较麻烦
使用js对象模型创建节点过于复杂
使用字符串模板,然后使用innerHTML赋值;但是复用性不好,也不提示
<div id="container1">
<p is="my-text" text="大家好" id="myText"></p>
</div>
<script>
class MyText extends HTMLParagraphElement {
constructor() {
super();
}
connectedCallback(){
const content = `<div>触发了connectedCallback生命周期</div>`;
this.innerHTML = content;
}
}
window.customElements.define("my-text",MyText,{extends:'p'})
</script>
使用template模板,因为template内容在页面是不展示的
<template id="tpl-test">
<style type="text/css">
.title {
color:red;
}
</style>
<div class="title">标题</div>
<slot name="slot-des">默认内容</slot>
</template>
<test-item>
<div slot="slot-des">不是默认内容</div>
</test-item>
<script>
class TestItem extends HTMLElement {
constructor() {
super();
}
connectedCallback(){
const content = document.getElementById("tpl-test").content.cloneNode(true);
// 不能使用append
// this.append(content);
const shadow = this.attachShadow({mode:"open"});
shadow.append(content)
}
}
window.customElements.define("test-item",TestItem)
</script>
Shadow DOM
特点:
1、使用影子DOM可以隔离样式
2、影子DOM内部元素不可以被直接访问到;配置mode可以控制
mode:open:可以被访问
mode:closed:不可以被访问
访问使用:document.querySelector("product-item").shadowRoot
<body>
<div id="list"></div>
<template id="tpl-test">
<style type="text/css">
.title {
color:red;
}
</style>
<div class="list-item">
<div class="title">标题</div>
<div class="text"></div>
</div>
</template>
<script>
class TestItem extends HTMLElement {
constructor() {
super();
this.addEventListener("click",(e)=>{
console.log(e)
})
}
connectedCallback(){
const content = document.getElementById("tpl-test").content.cloneNode(true);
// 不能使用append
// this.append(content);
const shadow = this.attachShadow({mode:"open"});
content.querySelector(".list-item").setAttribute("id",this.id);
content.querySelector(".text").innerText = this.text;
shadow.appendChild(content)
}
}
window.customElements.define("test-item",TestItem)
</script>
<script>
const listData = [{
id:"1",
name:"文本1"
},{
id:"2",
name:"文本2"
}]
const listEl = document.getElementById("list");
function createListItem(attrs) {
const el = document.createElement("test-item");
el.text = attrs.name;
el.id = attrs.id;
return el;
}
const elList = listData.map(createListItem);
listEl.append.apply(listEl,elList)
</script>
</body>