《关于实现一个函数把真实dom转换成虚拟dom原来是这么一回事》
实现这个函数之前,首先要对js的一些官方api要有明确概念。
-
document.querySelector()
-
HTMLElement
-
HTML DOM nodeType 属性
HTML DOM Element 对象
HTML DOM 节点
在 HTML DOM (文档对象模型)中,每个部分都是节点:
-
文档本身是文档节点 -
所有 HTML 元素是元素节点 -
所有 HTML 属性是属性节点 -
HTML 元素内的文本是文本节点 -
注释是注释节点
Element 对象
在 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 中的节点数。 |
Node Types
文档、元素、属性以及 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 |
HTML DOM querySelector() 方法
语法
document.querySelector(CSS selectors)
参数值
参数 | 类型 | 描述 |
---|---|---|
CSS 选择器 | String | 必须。指定一个或多个匹配元素的 CSS 选择器。可以使用它们的 id, 类, 类型, 属性, 属性值等来选取元素。 对于多个选择器,使用逗号隔开,返回一个匹配的元素。 提示: 更多 CSS 选择器,请参阅 CSS 选择器参考手册。 |
真实dom转换成虚拟dom
思路
首先要明确,虚拟dom树上的一个节点VNode,需要哪些必要的数据。
-
标签名 -
元素类型 -
元素属性值(id、class、name等,字典形式) -
子节点(数组形式) -
文本
所以,我们的一个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.length; 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 < childNodes.length; 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里辨别出文本节点的其中一个方法。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="root" class="tt">
<div title="tt1">hello1</div>
<div title="tt2">hello2</div>
<div title="tt3">hello3</div>
<img src="http://img.netbian.com/file/2020/0904/c150c8a4fdf8762f51093f636c7de785.jpg" alt="xxx">
<p>
<span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span>
</p>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
</div>
<script>
// 用内存去表述DOM
// 将真实DOM转化为虚拟DOM
// <div /> => {tag:'div'} 元素转化
// 文本节点 => {tag:undefined,value:'文本节点'} 文本节点转化
// <div title="1" class="c" /> => { tag:'div',data:{ title:'1',class:"c" } } 多属性转化
// <div><div /></div> => {tag:'div',children:[{ tag:'div' }]}
// 用构造函数来 进行以上转换
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 < e.length; 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 < property.length; i++) {
_propertyObj[property[i].nodeName] = property[i].nodeValue;
}
_vnode = new VNode(nodeName, _propertyObj, undefined, nodeType)
let childNodes = node.childNodes;
for (let i = 0; i < childNodes.length; 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>
部分输出如下: