原生js实现DOM与虚拟DOM相互转化

1,862 阅读3分钟

前言

认认真真学习vue源码,从0到1。。。

像Vue、React 这样的框架,对于其模板转换成虚拟DOM,再进行后续操作的用法应该耳熟能详,那么怎么实现一个DOM和虚拟DOM的相互转换呢? 这里我用的是递归的方式简单的做转化,与Vue、React底层实现的方式不同,只是为了更好的了解DOM和虚拟DOM。

首先要了解为什么需要虚拟DOM?

由于在我们日常开发中,经常需要修改DOM,频繁操作真实DOM会触发重绘和重排,严重影响页面性能,所以提出了虚拟DOM,作为真实DOM的抽象,以对象的形式存储DOM信息,缓存在内存中,一次性更新真实DOM,提高性能。

准备知识

1. DOM是什么?

详细的DOM原生API可以参考MDN:DOM

DOM是 JavaScript 操作网页的接口,全称为“文档对象模型”, DOM模型用一个逻辑树来表示一个文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象(objects)。

节点(node)是DOM树的最小单位,文档的树形结构就是有各个不同列类型(Document、Element、Text)的节点组成的。

详细的NODE原生API参考MDN:NODE

在一个html文件中写一段简单的DOM结构
<div id="root" class="c1">
    <p>你好</p>
</div>
<script>
  const root = document.querySelector('#root')
  console.dir(root)
  console.dir(root.childNodes)
</script>

在控制台打印出DOM结构

image.png

2. NODE的属性

  1. nodeName:node的名字,如果是element那名字是大写的,其他的名字前面写上#。

  2. nodeType:node的类型,一般用数字表示,1表示element(也可以用Node.ELEMENT_NODE来表示),3表示text(Node.TEXT_NODE)。

    如果是element,那么nodeName === tagName

    如果是text,那么nodeName = #text, tagName = undefined

  3. nodeValue:当前节点的值,对于text, comment节点来说, nodeValue返回该节点的文本内容,对于 attribute 节点来说, 返回该属性的属性值,而对于document和element节点来说,返回null

image.png

真实DOM => 虚拟Dom

创建VDom类,包含属性: tag、data、value、type、children

从DOM树根节点开始,将每个节点实例化为一个VDom, 递归遍历其子节点

对于文本节点,没有子节点,只有一个文本属性

// 创建VDom类
class VDom {
    constructor(tag, data, value, type) {
      this.tag = tag && tag.toLowerCase() // 节点名
      this.data = data // 属性
      this.value = value // 文本数据
      this.type = type // 节点类型
      this.children = []
    }
    appendChild(vnode) {
      this.children.push(vnode)
    }
  }

  function getVNode(node) {
    let nodeType = node.nodeType
    let _vnode = null
    if (nodeType === 1) {
      // 元素
      let tag = node.nodeName
      let attrs = node.attributes
      let _attrObj = {}
      for (let i = 0; i< attrs.length; i++) {
        _attrObj[attrs[i].nodeName] = attrs[i].nodeValue
      }
      _vnode = new VDom(tag, _attrObj, undefined, nodeType)
      
      let children = node.childNodes
      for (let i = 0; i < children.length; i++) {
        _vnode.appendChild(getVNode(children[i]))
      }
    } else if (nodeType === 3) {
      // 文本
      _vnode = new VDom(node.nodeName, undefined, node.nodeValue, nodeType)
    }
    return _vnode
  }
  const vroot = getVNode(document.querySelector('#root'))
  console.log(vroot)

最终转换的VDom格式如下:

image.png

虚拟DOM => 真实DOM

这里有一个知识点,如果通过一个nodeName创建一个元素节点?

如何通过一段文本创建一个文本节点?

createElement: 通过指定名称创建一个元素

createTextNode:创建文本节点

思路和上面的类似,用递归的形式不断去创建DOM,并append到父组件中

function parseVNode(vnode) {
    let type = vnode.type
    let rdom = null
    if (type === 1) {
      rdom = document.createElement(vnode.tag)
      // 元素
      let attrs = vnode.data
      for (let key in attrs) {
        rdom.setAttribute(key, attrs[key])
      }
      let children = vnode.children
      for (let i = 0; i < children.length; i++) {
        rdom.appendChild(parseVNode(children[i]))
      }
    } else if (type === 3) {
      // 文本
      rdom = document.createTextNode(vnode.value)
    }
    return rdom
}
const realRoot = parseVNode(vroot)
console.log(realRoot)
document.body.appendChild(realRoot)

image.png

总结

本文主要是用递归的方式,用原生js实现真实DOM和虚拟DOM直接的转化,是很基础的知识点,现在很多人用框架久了不会使用原生方法了,做这些小练习对于了解原生js和阅读框架源码都有好处。

文章如有错误请大家及时指出,莫怪莫怪!

二话不多说,滚去继续学习啦~~~

image.png