什么是虚拟DOM

151 阅读4分钟

虚拟DOM

一、什么是虚拟DOM

虚拟DOM(Virtual DOM)本质上是一个能描述DOM结构的 JavaScript 对象。

虚拟 DOM 是对真实DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。

在代码渲染到页面之前,vue 或者 rect 会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实的 DOM 结构,最终渲染到页面,在每次数据发生变化前,虚拟 DOM 都会缓存一份,变化之时现在的虚拟 DOM 会和缓存的虚拟 DOM 进行比较。

当视图更新时,虚拟DOM会进行Diff比较,在内存中计算好要渲染的DOM,从而更新虚拟DOM树,然后再加载到真实DOM中。

另外,现代前端框架的一个基本要求就是无需手动操作 DOM ,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动操作 DOM 可以大大提高开发效率。

二、为什么要使用虚拟DOM

1.保证性能下限

页面渲染过程:解析 HTML => 生成 DOM => 生成 CSSOM => Layout(布局) => Paint(绘制) => Compiler

修改 DOM 时:

真实 DOM :生成 HTML 字符串 + 重建所有的 DOM 元素

虚拟 DOM :生成 Virtual Node + DOMDiff + 必要的 DOM 更新

优点:

  • 更新DOM时,虚拟DOM在JS中的准备阶段上耗费更多的时间,相比于直接操作真实DOM它的代价是很低的。
  • 首次渲染大量的 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。
  • 在后续大量、频繁的DOM操作时,使用虚拟DOM性能会更快。

2.跨平台

虚拟DOM 本质上是 JS 对象,他可以很方便的跨平台操作。比如 Node.js 就没有 DOM ,如果想实现 SSR(服务端渲染),那么,一个方式就是借助虚拟 DOM ,因为虚拟 DOM 本身就是 JS 对象。

三、虚拟DOM的diff算法

当DOM更新操作发生时,虚拟DOM不会立即操作DOM,而是将操作中更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性地渲染到DOM树上,节省了真实DOM的操作。

vue采用的是深度优先,同层比较的策略。

平层Diff,只有以下4种情况:

  1. 节点类型改变。直接将旧节点卸载并装载新节点。旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做效率不高。开发时应避免节点类型的变化。
  2. 节点类型不变,仅有属性或属性值改变。此时不会卸载节点,而是执行节点更新的操作。
  3. 文本内容改变。直接修改文字内容。
  4. 移动、增加、删除 子节点,根据key属性,找到具体位置,再执行卸载、加载操作。

四、vue中如何实现虚拟DOM

实现简易的创建虚拟DOM并渲染

<!-- 创建并插入以下html元素 -->
<div id="wrapper">
  	<span style="color: red">hello</span>
  	yjh
</div>
// index.js

import {h} from './h.js';
import {render} from './patch.js';

// 导出模块
export default {
    mounted() {
        // 创建虚拟节点
        let vnode = h('div', {id: 'wrapper', a: 1, key: 'xx'},
            h('span', {style: {color: 'red'}}, 'hello'),
            'yjh');

        // 将虚拟节点渲染至页面
        let yyy = document.getElementById('yyy')
        render(vnode, yyy);
    }
};

h函数,创建虚拟节点

/**
 * 创建虚拟节点
 * @param type 标签
 * @param props 属性
 * @param children 子节点
 * @return {{children: *, text: *, type: *, key: *, props: *}}
 */
function h(type, props, ...children) {
    let key;
    if (props.key) {
        key = props.key;
        delete props.key;
    }
    // 将不是虚拟节点的子节点 变成虚拟节点
    children = children.map(child => {
        if (typeof child === 'string') {
            return vnode(undefined, undefined, undefined, undefined, child);
        } else {
            return child;
        }
    });
    return vnode(type, key, props, children);
}

function vnode(type, key, props, children, text) {
    return {
        type, key, props, children, text
    };
}

export {h};

patch算法,将虚拟节点渲染到页面的容器中

/**
 * 将虚拟节点渲染到页面的容器中
 * @param vnode 虚拟节点
 * @param container 要渲染到的容器
 */
function render(vnode, container) {
    // 将虚拟节点转化成真实节点
    let ele = createDomElementFromVnode(vnode);
    console.log(ele)
    container.appendChild(ele);
}

function createDomElementFromVnode(vnode) {
    let {type, key, props, children, text} = vnode;
    if (type) {
        // 标签
        vnode.domElement = document.createElement(type);
        // 根据虚拟节点属性,去更新真实DOM
        updateProperties(vnode);
        // children中放的也是虚拟节点,递归渲染
        children.forEach(childVnode => render(childVnode, vnode.domElement))
    } else {
        // 文本
        vnode.domElement = document.createTextNode(text);
    }
    return vnode.domElement;
}

function updateProperties(newVnode, oldProps={}) {
    // 真实的DOM元素
    let domElemnt = newVnode.domElement;
    // 当前虚拟节点中的属性
    let newProps = newVnode.props;

    // 移除属性
    for (let oldPropName in oldProps) {
        if (!newProps[oldPropName]) {
            delete domElemnt[oldPropName];
        }
    }

    // 新节点属性直接覆盖
    for (let newPropsName in newProps) {
        if (newPropsName === 'style') {
            let styleObj = newProps.style;
            for (let s in styleObj) {
                domElemnt.style[s] = styleObj[s];
            }
        } else {
            domElemnt[newPropsName] = newProps[newPropsName];
        }
    }
}

export {render};