likes
comments
collection
share

深入剖析虚拟 DOM 原理

作者站长头像
站长
· 阅读数 2

最近看了《Vuejs设计与实现》这本书关于虚拟dom的一些章节,很有启发,个人感觉这本书讲得通俗易懂,对于一些想了解Vue底层知识的同学帮助很大,强烈推荐大家去看!!!

这篇文章主要讲解一下关于虚拟dom的前世今生吧,也可以说是我在深入学习完虚拟dom后对它一些理解。接下来会从以下方面来进行展开:

  1. 命令式和声明式
  2. 为什么会有虚拟dom
  3. 虚拟dom是什么以及是如何实现的

命令式和声明式

首先来聊一聊框架这个东西,我们所使用的框架分为命令式框架声明式框架,它们都各有优缺点。

命令式框架

命令式框架的特点就是关注过程,比较典型的就是jquery,那关注过程是什么意思呢?其实可以简单的理解为是我们写代码的过程,比如:

const box = document.getElementById("box");
box.innerText = 'Hello World !'
box.addEventListener('click',()=>{
    box.innerText = '你好 世界 !'
})

那在jquery中我们会这样写

$('#box').text('Hello World !').on('click',()=>{
    box.innerText = '你好 世界 !'
})

上面两段代码的作用可以翻译成:

  • 获取id名为box的div标签
  • 设置它的文本内容为"Hello World!"
  • 为其绑定一个点击事件
  • 当点击时修改box标签文本内容为"你好 世界!"

这里指的就是关注过程,给它下个定义就是我们所写的代码能够用自然语言描述出来或者说可以产生一一对应的关系

声明式框架

声明式框架的特点就是关注结果,比较典型的就是我们熟知的Vue、React这些框架,关注结果又是什么意思呢?其实可以理解为省略了操作dom的过程,这些都会放在框架内部操作,我们只需要操作最重要显示的数据即可,拿Vue举例:

<div @click="()=>console.log('Hello World!')"> Hello World ! <div>

因此可以看出Vue的内部实现是命令式的,暴露给用户的是声明式的。

两者对比

显而易见的是声明式代码的性能并不优于命令式代码,原因也很简单,声明式代码是在命令式代码的基础上实现的。比如说要修改一个div标签中的内容:

div.textContent = 'Hello virtualDom !'

我们会写出以上的命令式的代码,因为我们明确知道要修改的就是div标签中的元素,但是声明式的代码不一样,它需要找到前后的差异然后只更新变化的地方,相当于它需要先对比前后变化:

// 更新之前
<div @click="()=>console.log('Hello World!')"> Hello World ! <div>
    
// 更新之后
<div @click="()=>console.log('Hello World!')"> Hello virtualDom ! <div>

然后再进行div.textContent = 'Hello World !'进行更新,所以声明式代码会比命令式代码多出一步对比的过程(性能消耗),但是最终还是使用命令式代码来完成更新。所以声明式代码的性能并不优于命令式代码

既然命令式代码性能更好为什么Vue还要选择声明式呢?原因就在于声明式代码可维护性强所以Vue就是看中了这点,想象一下如果使用命令式代码,像创建、更新、删除dom等这些工作都需要我们自己来做的话结果肯定是不言而喻的,但是使用声明式代码这个过程我们就不需要关心内部已经帮我们做好了,只需要关注结果就可以。所以Vue其实做到了在保持可维护性的同时让性能损失最小化。

虚拟DOM

出现原因

我们知道在vue中有数据双向绑定的功能,即数据驱动视图,但是使用真实DOM是没有办法直接跟数据进行关联的

<h1> {{ text }} </h1>

也就是说原生的html标签根本不知道我使用的是哪一个数据,所以就需要一个结构来对真实DOM进行描述,根据这个结构来判定数据与真实DOM的关系,当数据变化了我可以知道页面中的哪些数据变化了,相当于有了一个桥梁,这个结构就是虚拟DOM。

还有就是当数据变化时,真实DOM在更新时只有一种方法就是将整个DOM结构重新渲染,因为DOM操作是非常消耗性能的,所以当数据变化后需要生一个新的虚拟DOM结构与之前的虚拟DOM结构进行对比,找出差异后对应真实DOM进行更新,但是使用虚拟DOM会提高性能只是理论上的,因为中间还多出了对比(层层对比)的过程。

虚拟DOM-->真实DOM

接下里再聊聊虚拟DOM是怎么一步步转换为真实DOM的,首先虚拟DOM就是一个JS对象,是一个对真实DOM的抽象,即通过属性的方式来描述节点信息。

深入剖析虚拟 DOM 原理

最终就是要把真实DOM抽象成以上(如上图)的JS对象,下面来实现一个从模板到虚拟DOM再到页面渲染的过程(即构建虚拟节点->转为真实DOM->渲染真实DOM)。

首先先准备好一个根标签

<div id="#app"></div>

模板 -> 虚拟DOM

首先我们需要使用createElement方法来创建出虚拟DOM节点

import { createElement } from './virtualDom'

const vdom=createElement('ul', {
    class: 'list',
    style: 'width:100px;height:100px;background:red'
}, [
    createElement('li', {
        class: "item",
    }, [
        createElement('p', { class: 'text' }, ['第一个列表项'])
    ]),
    createElement('li', {
        class: "item",
    }, [
        createElement('p', { class: 'text' }, ['第二个列表项'])
    ]),
    createElement('li', {
        class: "item",
    }, [
        createElement('p', { class: 'text' }, ['第三个列表项'])
    ])
])
console.log(vdom)

createElement方法共接收三个参数分别是节点类型、节点属性、节点的子节点,实现这个方法也很简单,实际上就是一个将每个节点生成对象的过程,这一步可以通过Class实例化对象来实现,下面通过在virtualDom.js文件中实现一下该方法。

// Element.js
class Element {
    constructor(type, prop, children) {
        this.type = type;
        this.prop = prop;
        this.children = children;
    }
}
export default Element;

// virtualDom.js
import Element from './Element.js'
function createElement(type, prop, children) {
    return new Element(type, prop, children)
}
export {
    createElement
}

这几步就完成了对虚拟节点的构建,也是非常的简单,然后就可以打印出上面图片中的结构了,下面开始将虚拟DOM转为真实DOM。

虚拟DOM -> 真实DOM

转换真实DOM的过程就是通过对虚拟节点进行遍历以及递归操作,在virtualDom.js文件中继续增加render方法专门用来转换真实DOM。

function render(vDom) {
    const el = document.createElement(vDom.type)
    const { props, children } = vDom;
    for (let key in props) {
        setAttrs(el, key, props[key])
    }
    children.forEach(c => {
        c = c instanceof Element ? render(c) : document.createTextNode(c)
        el.appendChild(c);
    })
    return el
}

首先要先根据最外层虚拟节点的type创建出父节点,然后将所有的props属性添加到节点上,传递的属性肯可能会有很多所以我们写个设置不同属性方式的方法setAttr来处理,然后开始遍历下面的子节点,子节点下面可能还会有元素节点所以需要进行递归,通过判断该元素节点是否是由Element类创建出来的,如果是的话需要重新遍历该虚拟节点,直到返回的是一个文本节点,最后将改结构返回。

function setAttrs(node, key, value) {
    switch (key) {
        // 处理属性为valud的情况
        case 'value':
            if (node.tagname == 'TEXTAREA') {
                node.value = value;
            } else {
                node.setAttribute(key, value)
            }
        case 'style':
            node.style.cssText = value;
            break;
        default:
            node.setAttribute(key, value);
            break;
    }
}

index.js文件中只需要调用render函数,将虚拟节点传入即可。

const rDom = render(vNode);
console.log(rDom)

到这里真实DOM的结构就已经出来了,接下来只需要渲染到页面上即可。

渲染真实DOM

要将真实DOM添加到页面上首先我们需要找到根元素,也就是#app,在virtualDom.js文件中继续增加renderDom方法专门用来渲染真实DOM。

function renderDom(rdom, rootdom) {
    rootdom.appendChild(rdom)
}

index.js文件中只需要调用个方法并将真实DOM和根元素传入即可完成真实DOM的渲染。

renderDom(rDom, document.getElementById("app"))

此时就可以在页面上看到效果啦!更多内容还在探索中......