节点的增加
要新创建节点,可以有以下几个函数:
- document.createElement,
- innerHTML,
- insertAdjacentHTML,
- insertAjacentText,
- createContextualFragment
下面是使用createContextualFragment模拟insertAdjacentHTML的一个版本
<body>
<div id="app">
这是app
<span>这是p2</span>
</div>
<p class="p1">这是p1</p>
<p class="p2">这是p2</p>
<p class="p3">这是p3</p>
<p class="p4">这是p4</p>
<script>
if (
typeof HTMLElement !== 'undefined' &&
!HTMLElement.prototype.insertAfter
) {
HTMLElement.prototype.insertAfter = function (ele, referenceNode) {
let nextSiblingEle = referenceNode.nextSibling;
if (!nextSiblingEle) {
//没有的化等价于appenchild
return this.appendChild(ele);
}
return this.insertBefore(ele, nextSiblingEle);
}
}
if (
typeof HTMLElement !== "undefined" &&
!HTMLElement.prototype.insertAdjacentElement
) {
const insertAdjacentMap = {
"beforebegin": function (ele) {
//插入元素自身的前面
if (!this.parentNode) {
return null;
}
return this.parentNode.insertBefore(ele, this);
},
"afterbegin": function (ele) {
//添加在元素内部第一个节点
if (!this.firstChild) {
return this.appendChild(ele);
}
return this.insertBefore(ele, this.firstChild);
},
"beforeend": function (ele) {
//添加在元素内部最后一个节点之后
return this.insertAfter(ele, this.lastChild);
},
"afterend": function (ele) {
return this.parentNode.insertAfter(ele, this);
}
};
HTMLElement.prototype.insertAdjacentElement = function (where, ele) {
where = where.toLowerCase();
if (!insertAdjacentMap[where]) {
throw Error("插入失败, 没有相应的规则!!")
}
return insertAdjacentMap[where].call(this, ele);
}
HTMLElement.prototype.insertAdjacentHTML = function (where, htmlStr) {
const range = this.ownerDocument.createRange();
range.setStartBefore(this);
let parseHtml = range.createContextualFragment(htmlStr);
this.insertAdjacentElement(where, parseHtml);
}
HTMLElement.prototype.insertAdjacentText = function (where, htmlText) {
let parseTextNode = this.createTextNode(htmlText);
this.insertAdjacentElement(where, parseTextNode);
}
}
//createContextualFragment
const app = document.querySelector("#app");
const span = document.querySelector("span");
const p1 = document.querySelector(".p1");
const p2 = document.querySelector(".p2");
const p3 = document.querySelector(".p3");
const p4 = document.querySelector(".p4");
//把p1插入到app之前
app.insertAdjacentElement("beforebegin", p1);
//把p2插入到app内部第一个节点之前
app.insertAdjacentElement("afterbegin", p2);
//把p3插入到app内最后一个节点之后
app.insertAdjacentElement("beforeend", p3);
//把p4插入到app之后
app.insertAdjacentElement("afterend", p4);
//把动态html插入到app之后
app.insertAdjacentHTML("afterend", "<p style='color:red;'>这是动态html插入的p</p>");
//把text插入到app之后
app.insertAdjacentText("afterend", "<p>这是静态p</p>");
</script>
</body>
createContextualFragment是一个把符合html字符串的参数转换成fragment片段的函树,也可以自己写一个将字符串转换为节点的函数,它可以实现script内容抽取
//判断是否是html字符串
const rhtml = /<|&#?\w;/;
//判断是否是但标签
const rsingleTag = /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;
const merge = function (arr1, arr2) {
let len = arr1.length;
for (let i = 0, item; item = arr2[i]; i++) {
arr1[len++] = item;
}
return arr1;
};
//提取标签名
const isArrayLike = function (item) {
if (typeof item === 'function' || item === item.window) {
return false;
}
return typeof item.length === 'number' && typeof item === 'object';
}
//排除函数, 函数的length表示形式参数的个数
//window也有length
const buildFragment = function (data, context, scripts) {
const nodes = [];
const fragment = context.createDocumentFragment();
let tmp;
for (let i = 0, item; item = data[i]; i++) {
if (typeof item === 'object' && (item.nodeType || isArrayLike(item))) {
merge(nodes, item.nodeType ? [item] : item);
} else if (!rhtml.test(item)) {
nodes.push(context.createTextNode(item));
} else {
//解析html字符串
tmp || (tmp = fragment.appendChild(context.createElement('div')));
tmp.innerHTML = item;
merge(nodes, tmp.childNodes);
tmp.textContent = "";//清空内容
}
}
fragment.textContent = "";
for (let i = 0, item; item = nodes[i]; i++) {
tmp = fragment.appendChild(item)
}
if (scripts) {
let eles = merge([], fragment.querySelectorAll("script"));
scripts.push(...eles);
}
return fragment;
};
const parseHTML = function (data, context, isKeepScript) {
if (typeof data !== "string") {
return [];
}
if (typeof context == "boolean") {
isKeepScript = context;
context = false;
}
if (!context) {
context = (new window.DOMParser()).parseFromString("", "text/html");
}
let parsed;
parsed = rsingleTag.exec(data);
if (parsed) {
return [context.createElement(parsed[1])];
}
let scripts = !isKeepScript && [];
parsed = buildFragment([data], context, scripts);
console.log(scripts, 'scripts');
if (scripts && scripts.length) {
for (let i = 0, item; item = scripts[i]; i++) {
item.remove();
}
}
console.log(parsed.childNodes, 'parsed.childModes');
return merge([], parsed.childNodes);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./index.js"></script>
<script >
const appEle = document.querySelector("#app");
appEle.appendChild.apply(appEle, parseHTML("<div>这是div</div>"));
appEle.appendChild.apply(appEle, parseHTML("我不是药神"));
//保留
const eles = parseHTML(`
<div style="color:red">这是red</div>
<script>
console.log('script!!');
<\/script>
`);
for (let i = 0, item; item = eles[i];i ++) {
appEle.appendChild(item);
}
//保留
const eles2 = parseHTML(`
<div style="color:red">这是red</div>
<script>
console.log('script!!');
<\/script>
`, true);
for (let i = 0, item; item = eles[i];i ++) {
appEle.appendChild(item);
}
for (let i = 0, item; item = eles2[i];i ++) {
appEle.appendChild(item);
}
</script>
</body>
</html>
更简单一点的实现如下
const parseHTML = function () {
const doc = new DOMParser().parseFromString(htmlString, 'text/html');
const fragment = document.createDocumentFragment();
fragment.append(...doc.body.childNodes);
return fragment;
};
节点的插入
关于节点的插入方法有以下这些 insertBefore,appendChild,replaceChild,insertAdjacentHTML,insertAdjacentText,insertAdjacentElement,append,prepend, after, before, replaceWith, jquery还提供了wrap,wrapAll,wrappInner这种操作,以下是模拟的实现
if (typeof HTMLElement !== "undefined" && !document.documentElement.removeNode) {
HTMLElement.prototype.removeNode = function (deep = false) {
const parent = this.parentNode;
if (!!deep) {
parent.removeChild(this);
} else {
const childNodes = this.childNodes;
const fragment = this.ownerDocument.createDocumentFragment();
while (childNodes.length) {
fragment.appendChild(childNodes[0]);
}
parent.replaceChild(fragment, this);
}
return this;
}
}
if (typeof HTMLElement !== "undefined" && !document.documentElement.applyElement) {
HTMLElement.prototype.applyElement = function (newNode, where) {
//removeNode,不移除子孙
if (newNode.parentNode) {
newNode = newNode.removeNode(false);
}
const range = this.ownerDocument.createRange();
where = (where || "outside").toLowerCase();
//选择整个outside
let method = where === 'outside' ? "selectNode" : (where === "inside" ? "selectNodeContents" : "error");
if (method === 'error') {
throw new Error('where错误!');
}
range[method](this);
range.surroundContents(newNode);
return newNode;
}
}
if (typeof HTMLElement !== "undefined" && !document.documentElement.wrapAll) {
HTMLElement.prototype.wrap = function (newNode) {
return this.applyElement(newNode, "outside");
}
HTMLElement.prototype.wrapInner = function (newNode) {
return this.applyElement(newNode, "inside");
}
HTMLElement.prototype.wrapAll = function (wrapper, ...args) {
this.applyElement(wrapper, "outside");
this.after(...args);
}
}
下面在html中测试功能是否能够实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
p {
color: red;
}
</style>
</head>
<body>
<div id="app">
<p><span class="span3">这是一个p</span></p>
<div style="border: 1px solid #ececec;">
<span class="span1">我是span1</span>
<span class="span2">我是span2</span>
</div>
<span class="span4">
我在3秒后把所有的span都移动到当前标签后面
</span>
</div>
<!-- <script src="./index.js"></script> -->
<!-- <script >
const appEle = document.querySelector("#app");
appEle.appendChild.apply(appEle, parseHTML("<div>这是div</div>"));
appEle.appendChild.apply(appEle, parseHTML("我不是药神"));
//保留
const eles = parseHTML(`
<div style="color:red">这是red</div>
<script>
console.log('script!!');
<\/script>
`);
for (let i = 0, item; item = eles[i];i ++) {
appEle.appendChild(item);
}
//保留
const eles2 = parseHTML(`
<div style="color:red">这是red</div>
<script>
console.log('script!!');
<\/script>
`, true);
for (let i = 0, item; item = eles[i];i ++) {
appEle.appendChild(item);
}
for (let i = 0, item; item = eles2[i];i ++) {
appEle.appendChild(item);
}
</script> -->
<script src="./indx2.js"></script>
<script>
const p = document.querySelector("p");
const span4 = document.querySelector(".span4");
const span1 = document.querySelector(".span1");
const span2 = document.querySelector(".span2");
const span3 = document.querySelector(".span3");
// //在span2外部放一个p
span2.applyElement(p);
//在span1上也放一个p
span1.wrap(document.createElement("p"));
//在span内部放一个p
span3.wrapInner(document.createElement("p"));
setTimeout(() => {
const p1 = document.createElement("p");
p1.style["display"] = "flex";
p1.style["flex-direction"] = "column";
p1.style["column-gap"] = "20px";
span4.wrapAll(p1, span1, span2, span3);
}, 3000);
</script>
<script>
</script>
</body>
</html>
节点的复制
使用元素的cloneNode就能解决节点复制的问题了。
节点的移除
removeChild, remove, deleteContents,当我们想要清空节点内部的时候,有三种版本
const clearNode = function (node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
return node;
}
//使用deleteContents删除
const deleteRange = document.createRange();
const clearNode = function (node) {
deleteRange.setStartBefore(node.firstChild);
deleteRange.setEndAfter(node.lastChild);
deleteRange.deleteContents();
return node;
}
const clearNode = function (node) {
node.textContent = "";
return node;
}
当我们为元素节点绑定了许多数据与事件的时候,为了防止内存泄漏,我们会需要一个回调在节点被移除的时候执行一些清理操作 ,可以使用MutationObserver实现这个功能
const isDetached = function (ele) {
return !ele.ownerDocument.contains(ele);
}
const onRemove = function (ele, onclearCallBack) {
const observe = new MutationObserver(function () {
if (isDetached(ele)) {
onclearCallBack();
}
});
observe.observe(document, {
childList: true,
subtree: true
});
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>图片库性能优化</title>
</head>
<body>
<div id="app">
这是app
</div>
<p class="p1">这是p1</p>
<script>
const isDetached = function (ele) {
return !ele.ownerDocument.contains(ele);
}
const onRemove = function (ele, onclearCallBack) {
const observe = new MutationObserver(function () {
if (isDetached(ele)) {
onclearCallBack();
}
});
observe.observe(document, {
childList: true,
subtree: true
});
}
const p1 = document.querySelector(".p1");
onRemove(p1, function () {
console.log("执行清理操作");
});
setTimeout(() => {
p1.remove();
}, 3000);
</script>
</body>
</html>