JS知识点回顾——Node节点

176 阅读11分钟

Node

是一个接口,各种类型的DOM API对象都会从这个接口继承;有继承就有原型

先看一下都有哪些DOM

image.png

查找一下原型链

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;
}

image.png

可以看到元素节点以及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>

image.png

3、使用getAttribute获取属性

4、使用setAttribute设置属性

文本

NodeType为3,对象模型是Text

注意:

空白的文本节点(换行或者空格引起),空白的文本节点也是可以被获取的

需要使用childNodes访问

使用nodeValue取值

<div id="root">啊哈<span>嗯哼</span></div>

image.png

<div id="root">
    啊哈
    <span>嗯哼</span>
</div>

image.png

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>

image.png

文档

NodeType为9,对象模型是Document

image.png

方法和属性

节点查找: 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)

image.png

文档类型

NodeType为10;对象模型是DocumentType;访问方式:document.doctype、document.firstChild;有用的属性只有name,返回"html"

如果头部有注释,firstChild返回的是注释

image.png

文档碎片

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>

image.png

元素的查找

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"))

image.png

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)

image.png

const inner2 = select.querySelectorAll(":scope .outer .inner");
console.log(console.log(inner2))

image.png

选择的是静态元素

<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

总结

image.png

特殊的查询

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>

image.png

元素新增、删除、复制

新增

节点的创建和挂载,创建了节点未加入文档是没有任何视觉效果的;所以新增节点得两部:创建和挂载

显示创建:

1、对象模型直接new

const commentNode = new Comment("这是注释");
const text = new Text("这是文本");
document.body.append(commentNode)
document.body.append(text)

image.png

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);

image.png

节点挂载

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,将节点插入给定的一个位置

参照节点是自身,要求传入的是元素,并且只能传入一个节点

image.png

<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"));

image.png

insertAdjacentHTML,将文本解析为节点并插入到给定的位置;参照节点是自身,要求传入的是字符串,语法和​insertAdjacentElement一致

insertAdjacentText,将文本节点插入到给定的位置;参照节点是自身,要求传入的是文本字符串,语法和insertAdjacentElement一致

replaceChildren,将后代替换为指定节点

replaceWidth,将后代替换为指定节点集合

对比

image.png

image.png

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将不会被挂载到全局作用域,一段时间后会被回收

image.png

使用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);
})()

image.png

节点近亲属性对比

HTMLElement.innerText:表示一个节点以及后代被渲染的文本内容,强调被渲染

Node.textContent:表示一个节点以及其后代节点的文本内容

区别:innerText可操作已经被渲染的内容,textContent不会;innerText受样式影响会触发浏览器绘制部分或全部页面,带来性能问题

image.png

Node.nodeValue:对于text、comment和CDATA节点来说,nodeValue返回该节点的文本内容;对于attribute节点来说,返回该属性的属性值

value:特定的一些HTMLElement元素,用value属性来获取其值,一般使用于form表单元素

image.png

clientWidth、offsetWidth、scrollWidth

Element.clientWidth(元素宽度),包含width+左右padding(不包含滚动条、border、margin、滚动条)

HTMLElement.offsetWidth(元素布局宽度),包含width+左右padding+左右border+滚动条(不包含margin)

Element.scrollWidth(元素内容宽度),用于测量元素内容宽度,包含由于overflow溢出而在屏幕上不可见的内容

image.png

Node.comperDocumentPosition:比较当前节点与任意文档中的另一个节点的位置,返回数字,带组合意义的数据,不仅可以返回包含信息还能返回在之前或者之后等信息;语法:compareMask = node.compareDocumentPosition(outerNode)

Node.contains:返回的是一个布尔值,来表示传入的节点是否为该节点的后代节点,仅能返回包含信息

image.png

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>

image.png

放在注册后面就不会生效,可以把对节点的处理放到生命周期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>

image.png

三项主要技术

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>

image.png

生命周期

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>

首次刷新触发

image.png

更新属性触发

image.png

删除节点触发

image.png

恢复节点触发

image.png

image.png

节点创建比较麻烦

使用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>

image.png

Shadow DOM

image.png

特点:

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>

image.png