实现这个函数之前,首先要对js的一些官方api要有明确概念。
document.querySelector()
HTMLElement
HTML DOM nodeType 属性
在 HTML DOM (文档对象模型)中,每个部分都是节点:
在 HTML DOM 中,Element 对象表示 HTML 元素。
Element 对象可以拥有类型为元素节点、文本节点、注释节点的子节点。
NodeList 对象表示节点列表,比如 HTML 元素的子节点集合。
元素也可以拥有属性。属性是属性节点(参见下一节)。
下面的属性和方法可用于所有 HTML 元素上:
属性 / 方法 | 描述 |
---|---|
element.accessKey | 设置或返回元素的快捷键。 |
element.appendChild() | 向元素添加新的子节点,作为最后一个子节点。 |
element.attributes | 返回元素属性的 NamedNodeMap。 |
element.childNodes | 返回元素子节点的 NodeList。 |
element.className | 设置或返回元素的 class 属性。 |
element.clientHeight | 返回元素的可见高度。 |
element.clientWidth | 返回元素的可见宽度。 |
element.cloneNode() | 克隆元素。 |
element.compareDocumentPosition() | 比较两个元素的文档位置。 |
element.contentEditable | 设置或返回元素的文本方向。 |
element.dir | 设置或返回元素的内容是否可编辑。 |
element.firstChild | 返回元素的首个孩子。 |
element.getAttribute() | 返回元素节点的指定属性值。 |
element.getAttributeNode() | 返回指定的属性节点。 |
element.getElementsByTagName() | 返回拥有指定标签名的所有子元素的集合。 |
element.getFeature() | 返回实现了指定特性的 API 的某个对象。 |
element.getUserData() | 返回关联元素上键的对象。 |
element.hasAttribute() | 如果元素拥有指定属性,则返回true,否则返回 false。 |
element.hasAttributes() | 如果元素拥有属性,则返回 true,否则返回 false。 |
element.hasChildNodes() | 如果元素拥有子节点,则返回 true,否则 false。 |
element.id | 设置或返回元素的 id。 |
element.innerHTML | 设置或返回元素的内容。 |
element.insertBefore() | 在指定的已有的子节点之前插入新节点。 |
element.isContentEditable | 设置或返回元素的内容。 |
element.isDefaultNamespace() | 如果指定的 namespaceURI 是默认的,则返回 true,否则返回 false。 |
element.isEqualNode() | 检查两个元素是否相等。 |
element.isSameNode() | 检查两个元素是否是相同的节点。 |
element.isSupported() | 如果元素支持指定特性,则返回 true。 |
element.lang | 设置或返回元素的语言代码。 |
element.lastChild | 返回元素的最后一个子元素。 |
element.namespaceURI | 返回元素的 namespace URI。 |
element.nextSibling | 返回位于相同节点树层级的下一个节点。 |
element.nodeName | 返回元素的名称。 |
element.nodeType | 返回元素的节点类型。 |
element.nodeValue | 设置或返回元素值。 |
element.normalize() | 合并元素中相邻的文本节点,并移除空的文本节点。 |
element.offsetHeight | 返回元素的高度。 |
element.offsetWidth | 返回元素的宽度。 |
element.offsetLeft | 返回元素的水平偏移位置。 |
element.offsetParent | 返回元素的偏移容器。 |
element.offsetTop | 返回元素的垂直偏移位置。 |
element.ownerDocument | 返回元素的根元素(文档对象)。 |
element.parentNode | 返回元素的父节点。 |
element.previousSibling | 返回位于相同节点树层级的前一个元素。 |
element.removeAttribute() | 从元素中移除指定属性。 |
element.removeAttributeNode() | 移除指定的属性节点,并返回被移除的节点。 |
element.removeChild() | 从元素中移除子节点。 |
element.replaceChild() | 替换元素中的子节点。 |
element.scrollHeight | 返回元素的整体高度。 |
element.scrollLeft | 返回元素左边缘与视图之间的距离。 |
element.scrollTop | 返回元素上边缘与视图之间的距离。 |
element.scrollWidth | 返回元素的整体宽度。 |
element.setAttribute() | 把指定属性设置或更改为指定值。 |
element.setAttributeNode() | 设置或更改指定属性节点。 |
element.setIdAttribute() | |
element.setIdAttributeNode() | |
element.setUserData() | 把对象关联到元素上的键。 |
element.style | 设置或返回元素的 style 属性。 |
element.tabIndex | 设置或返回元素的 tab 键控制次序。 |
element.tagName | 返回元素的标签名。 |
element.textContent | 设置或返回节点及其后代的文本内容。 |
element.title | 设置或返回元素的 title 属性。 |
element.toString() | 把元素转换为字符串。 |
nodelist.item() | 返回 NodeList 中位于指定下标的节点。 |
nodelist.length | 返回 NodeList 中的节点数。 |
文档、元素、属性以及 HTML 或 XML 文档的其他方面拥有不同的节点类型。
存在 12 种不同的节点类型,其中可能会有不同节点类型的子节点:
节点类型 | 描述 | 子节点 | |
---|---|---|---|
1 | Element | 代表元素 | Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference |
2 | Attr | 代表属性 | Text, EntityReference |
3 | Text | 代表元素或属性中的文本内容。 | None |
4 | CDATASection | 代表文档中的 CDATA 部分(不会由解析器解析的文本)。 | None |
5 | EntityReference | 代表实体引用。 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
6 | Entity | 代表实体。 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
7 | ProcessingInstruction | 代表处理指令。 | None |
8 | Comment | 代表注释。 | None |
9 | Document | 代表整个文档(DOM 树的根节点)。 | Element, ProcessingInstruction, Comment, DocumentType |
10 | DocumentType | 向为文档定义的实体提供接口 | None |
11 | DocumentFragment | 代表轻量级的 Document 对象,能够容纳文档的某个部分 | Element, ProcessingInstruction, Comment, Text, CDATASection, EntityReference |
12 | Notation | 代表 DTD 中声明的符号。 | None |
对于每种节点类型,nodeName 和 nodeValue 属性的返回值:
节点类型 | nodeName 返回 | nodeValue 返回 | |
---|---|---|---|
1 | Element | 元素名 | null |
2 | Attr | 属性名称 | 属性值 |
3 | Text | #text | 节点的内容 |
4 | CDATASection | #cdata-section | 节点的内容 |
5 | EntityReference | 实体引用名称 | null |
6 | Entity | 实体名称 | null |
7 | ProcessingInstruction | target | 节点的内容 |
8 | Comment | #comment | 注释文本 |
9 | Document | #document | null |
10 | DocumentType | 文档类型名称 | null |
11 | DocumentFragment | #document 片段 | null |
12 | Notation | 符号名称 | null |
document.querySelector(CSS selectors)
参数 | 类型 | 描述 |
---|---|---|
CSS 选择器 | String | 必须。指定一个或多个匹配元素的 CSS 选择器。可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。 对于多个选择器,使用逗号隔开,返回一个匹配的元素。 提示: 更多 CSS 选择器,请参阅 CSS 选择器参考手册。 |
首先要明确,虚拟dom树上的一个节点VNode,需要哪些必要的数据。
所以,我们的一个VNode节点,可以设置成一个这样的数据结构,这里用es6的class语法来实现。
class VNode {
constructor(tag, attrs, value, type) {
this.tag = tag && tag.toLowerCase(); // 标签名
this.attrs = attrs; // 属性值
this.value = value; // 文本
this.type = type; // 元素类型
this.children = [] // 子节点
}
// 追加子节点
appendChild(vnode) {
this.children.push(vnode)
}
}
然后,上文中的node type告诉我们:
文档、元素、属性以及 HTML 或 XML 文档的其他方面拥有不同的节点类型。
存在 12 种不同的节点类型,其中可能会有不同节点类型的子节点
所以判断一个node是文本节点还是元素结点时,可以用node.nodeType == 1
和node.nodeType == 3
那么接下来就很简单了,无非就是遍历一棵n叉树,使用递归就很容易实现,只是在递归的时候,加入node.nodeType == 1
和node.nodeType == 3
这两个判断,然后遇到文本情况跳出递归栈就可以了。
再细化到每个节点的拷贝,就是针对上面提到的几个必要属性,值得注意的是,属性也是一个节点。从官方api中拿数据填到vnode节点中,这个就是一个简单的属性深拷贝,这里就不再赘述了,直接上代码。
function getVNode(node) {
let nodeType = node.nodeType;
let _vnode = null;
// 对节点进行判断
if (nodeType === 1) {
// 元素节点
let nodeName = node.nodeName;
// 属性,返回属性组成的数组,我们就是把这个伪数组转换为对象
let attrs = node.attributes
let _attrObj = {};
// 循环attrs
for (let i = 0; i // attrs[i]是一个属性节点,我们要的是nodeName这个属性
_attrObj[attrs[i].nodeName] = attrs[i].nodeValue
}
_vnode = new VNode(nodeName, _attrObj, undefined, nodeType);
// 考虑node的子元素
let childNodes = node.childNodes;
for (let i = 0; i //递归
_vnode.appendChild(getVNode(childNodes[i]));
}
} else if (nodeType === 3) {
// 文本节点
_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
}
return _vnode;
}
由于文本也是一个节点。
**元素本身的nodeValue本来就是null,nodeValue是针对#text的。而且最重要的是nodeValue属性是用来获取文本节点的值的。**这也就意味着,nodeValue拿不到非文本节点的值。
一般情况下,打印文本节点,会出现#text
字样,这就是在console里辨别出文本节点的其中一个方法。
部分输出如下:html>
Titletitle>
head>
1span>2span>3span>4span>5span>6span>
p>
ul>
div>
// 用内存去表述DOM// 将真实DOM转化为虚拟DOM// // 文本节点 => {tag:undefined,value:'文本节点'} 文本节点转化// // // 用构造函数来 进行以上转换class VNode {constructor(tag, attrs, value, type) {this.tag = tag && tag.toLowerCase(); // 标签名this.attrs = attrs; // 属性值this.value = value; // 文本this.type = type; // 元素类型this.children = [] // 子节点
}// 追加子节点
appendChild(vnode) {this.children.push(vnode)
}
}//兼容浏览器获取节点文本的方法function getTextFromNode(e) {let t = "";//如果传入的是元素,则继续遍历其子元素//否则假定它是一个数组
e = e.childNodes || e;//遍历所有子节点for (let j = 0; j //如果不是元素,追加其文本值//否则,递归遍历所有元素的子节点
t += e[j].nodeType !== 1 ?
e[j].nodeValue : getTextFromNode(e[j].childNodes);
}//返回区配的文本return t;
}function getVirtualDom(node) {let nodeType = node.nodeType;let _vnode = null;if (nodeType === 1) {let nodeName = node.nodeName;let property = node.attributes;let _propertyObj = {};for (let i = 0; i _propertyObj[property[i].nodeName] = property[i].nodeValue;
}
_vnode = new VNode(nodeName, _propertyObj, undefined, nodeType)let childNodes = node.childNodes;for (let i = 0; i _vnode.appendChild(getVirtualDom(childNodes[i]))
}
} else if (nodeType === 3) {// 尤其要注意,文本也是一个节点// 可以回忆一下,原生js生成一个节点的步骤,就理解了let text = node.nodeValue
_vnode = new VNode(undefined, undefined, text, nodeType)
}return _vnode;
}let root = document.querySelector("#root");console.log(root);console.log(root.nodeType);console.log(root.attributes)// let vroot = getVNode(root);let vRoot = getVirtualDom(root)// console.log(vroot);console.log(vRoot)script>
body>
html>