vlambda博客
学习文章列表

《关于实现一个函数把真实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 == 1node.nodeType == 3

那么接下来就很简单了,无非就是遍历一棵n叉树,使用递归就很容易实现,只是在递归的时候,加入node.nodeType == 1node.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(undefinedundefined, node.nodeValue, nodeType);
    }
    return _vnode;
}

注意点:文本节点的判断和节点文本的获取

由于文本也是一个节点。

**元素本身的nodeValue本来就是null,nodeValue是针对#text的。而且最重要的是nodeValue属性是用来获取文本节点的值的。**这也就意味着,nodeValue拿不到非文本节点的值。

一般情况下,打印文本节点,会出现#text字样,这就是在console里辨别出文本节点的其中一个方法。

img

完整代码

<!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(undefinedundefined, 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>

部分输出如下:

img