虚拟DOM

本文解析了虚拟DOM的概念及其在提升前端性能方面的作用。介绍了浏览器渲染流程的局限性,并详细阐述了虚拟DOM如何通过减少不必要的DOM操作来提高性能。此外,还探讨了虚拟DOM的实现方式,包括构建虚拟DOM树的方法及diff算法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、引

最近开始看Vue的源码解析,看到了虚拟 DOM 这个关键词,一开始还是很懵逼的,所以打算开始学习这个东东,发现都已经出现四五年了。看了好多博客,统一的结论就是,它很快,比实际的 DOM 操作快很多。

那就让我们来看看吧!

二、先来看看浏览器里的 DOM

只要上百度查一下关于浏览器DOM的相关消息,几乎所有的文章都会贴一张浏览器渲染原理的图,这里就不放了,懒 O(∩_∩)O

大概分为下面5步:
1.浏览器的 HTML 解析器分析 HTML 文件,形成 DOM 树。
2.浏览器的 CSS 解析器分析 CSS 文件,形成样式表。
3.把 DOM 树和样式表结合,构建 Render 对象,最后形成一颗 Render 树。
4.浏览器根据 Render 树的信息开始在浏览器计算渲染位置。
5.根据 Render 树和位置信息开始通过节点的 Paint 方法渲染页面,形成网页。

看起来不难吧!,那挺好的,为什么还要虚拟的 DOM 呢???

因为 Jquery 和 JavsScrit 并不是很聪明,操作 DOM 时,浏览器会从构 建DO M树开始从头到尾执行一遍流程。比如当你在一次操作时,需要更新10个 DOM 节点,理想状态是一次性构建完 DOM 树,再执行后续操作。但浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。显然例如计算 DOM 节点的坐标值等都是白白浪费性能,可能这次计算完,紧接着的下一个 DOM 更新请求,这个节点的坐标信息就没了,前面的一次计算是无用功。

即使计算机硬件一直在更新迭代,操作 DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验。所以聪(tu)明(ding)的前端程序员发明出来了虚拟 DOM 概念,一开始是应用在 React.js ,后来在 Vue.js 2.0作者也把这个概念加入了。如同上面的操作,只需要把需要操作的 DOM 节点在 JS 中构建完成,然后使用 diff 算法,对浏览器中的节点进行更新,一次性实现节点的更新渲染。十次的 JS 计算比十次的 DOM 操作的计算简单的很多,性能也就提高了。

三、实现虚拟 DOM

先写个简单的页面

<div id="container">
    <p>P content</p>
    <div>div dom</div>
    <ul>
        <li class="item">Item</li>
        <li class="item">Item</li>
        <li class="item">Item</li>
    </ul>
</div>

然后用 JavsScript 模拟上面的节点信息。

const tree = Element('div', { id: 'container' }, [
    Element('p', {}, ['P content']),
    Element('div', {}, ['div dom']),
    Element('ul', {}, [
        Element('li', { class: 'item' }, ['Item']),
        Element('li', { class: 'item' }, ['Item']),
        Element('li', { class: 'item' }, ['Item']),
    ]),
]);

const root = tree.render();
document.getElementById('virtualDom').appendChild(root);

大概的 tree 对象像是上面真实 DOM节点的模仿写法,但是到底是什么意思呢?接下来慢慢学习。

先来看下 Element 函数是干嘛的:

function Element(tagName, props, children) {
    //tagName 节点名
    //props   节点属性
    //children节点子节点信息
    if (!(this instanceof Element)) {
        return new Element(tagName, props, children);
    }
    //保证了调用格式 var el = new Element 
    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
    this.key = props ? props.key : undefined;

    let count = 0;
    this.children.forEach((child) => {
        if (child instanceof Element) {
            count += child.count;
        }
        count++;
    });
    this.count = count;
}

现在在 JavaScript 中已经保存了完整的虚拟 DOM 信息,下一步就是把虚拟的变成的实际的,那么久直接调用 JavaScript 的原生函数 document.createElement() 即可。

Element.prototype.render = function(){
    let el = document.createElement(this.tagName);
    let props = this.props;

    for (let propName in props) {
        setAttr(el, propName, props[propName]);
    }

    this.children.forEach((child) => {
        let childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);
        el.appendChild(childEl);
    });

    return el;
};

这个 Render() 函数就很简单了,将 DOM 的属性全都加到 DOM 元素上,如果有子元素继续递归调用创建子元素,并 appendChild() 挂到该 DOM 元素上。这样就完成了 DOM 的由虚到实。

四、Diff 算法

虚拟 DOM 中,在 DOM 的状态发生变化时,虚拟 DOM 会进行 Diff 运算,来更新只需要被替换的 DOM ,而不是全部重绘。在 Diff 算法中,只平层的比较前后两棵 DOM 树的节点,并没有进行深度的遍历,事件复杂度也就从 O(n3) 降低到了 O(n) 。

以下就是 Diff 操作的几个可能情况

1.如果节点类型改变,直接将旧节点卸载,替换为新节点,旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做也是效率不高的一个地方。
2.节点类型不变,属性或者属性值改变,不会卸载节点,执行节点更新的操作。
3.文本改变,直接修改文字内容。
4.移动,增加,删除子节点

具体的可以再仔细查阅资料,就不画图出来了。

五、映射

上面已经得到了实际的 DOM 对象和虚实 DOM 间的差异,就可以对照着进行映射更新浏览器内容了。使用 DP 把 DOM 的 Diff 内容更新。

function dfsWalk(node, walker, patches) {
    const currentPatches = patches[walker.index];

    const len = node.childNodes ? node.childNodes.length : 0;
    for (let i = 0; i < len; i++) {
        walker.index++;
        dfsWalk(node.childNodes[i], walker, patches);
    }

    if (currentPatches) {
        applyPatches(node, currentPatches);
    }
}
function applyPatches(node, currentPatches) {
    currentPatches.forEach((currentPatch) => {
        switch (currentPatch.type) {
            case REPLACE: {
                const newNode = (typeof currentPatch.node === 'string')
                    ? document.createTextNode(currentPatch.node)
                    : currentPatch.node.render();
                node.parentNode.replaceChild(newNode, node);
                break;
            }
            case REORDER:
                reorderChildren(node, currentPatch.moves);
                break;
            case PROPS:
                setProps(node, currentPatch.props);
                break;
            case TEXT:
                if (node.textContent) {
                    node.textContent = currentPatch.content;
                } else {
                    // ie
                    node.nodeValue = currentPatch.content;
                }
                break;
            default:
                throw new Error(`Unknown patch type ${currentPatch.type}`);
        }
    });
}

大功告成!!!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值