javaScript框架设计节点模块

3 阅读4分钟

节点的增加

要新创建节点,可以有以下几个函数:

  1. document.createElement,
  2. innerHTML,
  3. insertAdjacentHTML,
  4. insertAjacentText,
  5. 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>