美文网首页
简单模拟虚拟dom

简单模拟虚拟dom

作者: 大喵爱读书 | 来源:发表于2020-02-04 20:05 被阅读0次

虚拟dom是现在前端非常重要的一个技术,这篇文章试着简单模拟一下虚拟dom功能。
虚拟dom比较内存中虚拟dom的差异,然后通过差异来修改html中真实的dom节点,这中间对于dom进行最小修改从而减少页面的重排与重绘。

虚拟dom类

首先就要写一个虚拟dom的类来模拟html中真实节点:

class Element {
    constructor(tagName, attributes = {}, children = []) {
        this.tagName = tagName;
        this.attributes = attributes;
        this.children = children;
    }

    render() {
        const ele = document.createElement(this.tagName);
        if (this.children && this.children.length > 0) {
            this.children.forEach((childElement) => {
              // 简单判断是节点还是文案 
                if (childElement instanceof Element) {
                    ele.appendChild( childElement.render());
                } else if(typeof childElement === 'string') {
                    ele.appendChild(document.createTextNode(childElement));
                }
            })
        }
        return ele;
    }
}

方便测试在创建两个工具方法:

// 构建虚拟dom节点
const element = (tagName, attributes, children) => {
    return new Element(tagName, attributes, children);
};

const renderDom = (ele, dom) => {
    dom.appendChild(ele);
};

测试

image.png

虚拟dom比较方法


const compareDiff = (newVirtualDom, oldVirtualDom, index, patch) => {
    const diffresult = [];
    if (!newVirtualDom) {
        diffresult.push({
            type: NODE_OPERATOR_TYPE.REMOVE_NODE,
        })
    } else if (typeof newVirtualDom === 'string' && typeof oldVirtualDom === 'string') {
        if (oldVirtualDom !== newVirtualDom) {
            diffresult.push({
                type: NODE_OPERATOR_TYPE.MODIFY_TEXT,
                data: newVirtualDom
            })
        }
    } else if (newVirtualDom.tagName === oldVirtualDom.tagName) {
        const diffAttributeResult = {};
        // 先遍历老dom找出变化的属性值, 再遍历新dom找出新添加的属性
        for (const key in oldVirtualDom) {
            if (oldVirtualDom[key] !== newVirtualDom[key]) {
                diffAttributeResult[key] = newVirtualDom[key];
            }
        }

        for (const key in newVirtualDom) {
            if (!oldVirtualDom.hasOwnProperty(key)) {
                diffAttributeResult[key] = newVirtualDom[key];
            }
        }
        if (Object.keys(diffAttributeResult).length > 0) {
            diffresult.push({
                type: NODE_OPERATOR_TYPE.MODIFY_ATTRIBUTE,
                diffAttributeResult
            })
        }
        // 在节点相同情况下遍历子节点进行比较
        oldVirtualDom.children.forEach((childEle, index) => {
            compareDiff(newVirtualDom.children[index], childEle, ++initialIndex, patch)
        })
    } else {
        diffresult.push({
            type: NODE_OPERATOR_TYPE.REPLACE_NODE,
            newVirtualDom
        })
    }
    if (diffresult.length > 0) {
        patch[index] = diffresult;
    }
};

这里只是简单的进行了判断,有些方面考虑并不周全

工具方法

const diff = (oldVirtualDom, newVirtualDom) => {
    let patches = {}

    // 递归树 比较后的结果放到 patches
    compareDiff(oldVirtualDom, newVirtualDom, 0, patches)

    return patches
}

测试

    const fruitVirtualDom = element('ul', {id: 'list'}, [
        element('li', {class: 'fruit'}, ['苹果']),
        element('li', {class: 'fruit'}, ['橘子']),
        element('li', {class: 'fruit'}, ['葡萄'])
    ]);
    const fruitVirtualDom1 = element('ul', {id: 'list'}, [
        element('li', {class: 'fruit'}, ['苹果a']),
        element('div', {class: 'fruit'}, ['橘子']),
        element('li', {class: 'fruit'}, ['葡萄'])
    ]);
    const result = diff(fruitVirtualDom1, fruitVirtualDom)
    console.log(result)
image.png

这里没有递归对于属性对象进行深度判断,所以多了很多diff结果。

应用patch

得到虚拟dom差异后就可以应用在html中真实dom节点上了


function doPatch(node, diffResult) {
    diffResult.forEach((diff) => {
        switch (diff.type) {
            case NODE_OPERATOR_TYPE.REPLACE_NODE:
                const newVirtualDom = diff.newVirtualDom;
                let newNode = null;
                if (newVirtualDom instanceof Element) {
                    newNode = newVirtualDom.render();
                } else if (typeof newVirtualDom === 'string') {
                    newNode = document.createTextNode(newVirtualDom);
                }
                node.parentNode.replaceChild(newNode, node);
                break;
            case NODE_OPERATOR_TYPE.MODIFY_ATTRIBUTE:
                const diffAttributeResult = diff.diffAttributeResult;
                const attributes = diffAttributeResult.attributes;
                for (const attr in attributes) {
                    if (node.nodeType === 1) {
                        if (attributes[attr]) {
                            setAttribute(node, attr, attributes[attr]);
                        } else {
                            node.removeAttribute(attr);
                        }
                    }
                }
                break;
            case NODE_OPERATOR_TYPE.MODIFY_TEXT:
                node.textContent = diff.data;
                break;
            case NODE_OPERATOR_TYPE.REMOVE_NODE:
                node.parentNode.removeChild(node);
                break;
            default:
                break;
        }
    })
}

function walk(node, walker, patches) {
    const currentPatch = patches[walker.index];
    node.childNodes.forEach((node, index) => {
        walker.index++;
        walk(node, walker, patches);
    })
    if (currentPatch) {
        doPatch(node, currentPatch);
    }
}

const patch = (node, patches) => {
    const walker = {index: 0};
    walk(node, walker, patches);
}

这里还有一个工具方法setAttribute,对于原生的方法加了小小封装:

const setAttribute = (node, key, value) => {
    switch (key) {
        case 'style':
            node.style.cssText = value
            break
        case 'value':
            let tagName = node.tagName || ''
            tagName = tagName.toLowerCase()
            if (
                tagName === 'input' || tagName === 'textarea'
            ) {
                node.value = value
            } else {
                // 如果节点不是 input 或者 textarea, 则使用 setAttribute 去设置属性
                node.setAttribute(key, value)
            }
            break
        default:
            node.setAttribute(key, value)
            break
    }
}

最终测试

<button id="btn">change</button>
<script type="text/javascript">
    const fruitVirtualDom = element('ul', {id: 'list'}, [
        element('li', {class: 'fruit'}, ['苹果']),
        element('li', {class: 'fruit'}, ['橘子']),
        element('li', {class: 'fruit'}, ['葡萄'])
    ]);
    const ele = fruitVirtualDom.render();

    renderDom(ele, document.body)
    const btn = document.getElementById('btn');
    btn.addEventListener('click', () => {

        const fruitVirtualDom1 = element('ul', {id: 'list'}, [
  element('li', {class: 'fruit'}, ['apple']),
            element('li', {class: 'fruit'}, ['orange']),
            element('li', {class: 'fruit', style: "color:red"}, ['grape'])       ]);
        const patches = diff(fruitVirtualDom1, fruitVirtualDom)
        patch(ele, patches)
    }, false)
</script>

再点击button后对于页面进行更改:


点击前
点击后

结尾

这篇文章只是简单模拟一下虚拟dom的功能,还有很多问题没有考虑,大家在看的时候不要太过于纠结,理解这里边大体思想就好。

相关文章

  • 简单模拟虚拟dom

    虚拟dom是现在前端非常重要的一个技术,这篇文章试着简单模拟一下虚拟dom功能。虚拟dom比较内存中虚拟dom的差...

  • 实现一个简易的虚拟DOM

    虚拟DOM 虚拟DOM用原生的JavaScript模拟实现了DOM结构,.我们通过操作这个虚拟DOM树来实现对页面...

  • Day7:virtual dom & MVVM

    virtual dom 什么是virtual dom 虚拟dom 用JS模拟DOM结构 DOM变化的对比,放在JS...

  • JavaScript练习 - convertHTMLToJson

    在使用vue时,我们常说到的一个概念就是虚拟DOM。那么虚拟DOM是什么?虚拟DOM就是根据真实DOM模拟出的一个...

  • vdom

    vdom是什么 virtual dom , 虚拟 DOM 用 js 模拟DOM结构 为什么会存在vdom DOM操...

  • 虚拟DOM详解

    vdom是什么?为何使用vdom? virtual dom,虚拟DOM 用JS模拟DOM结构 DOM操作非常昂贵 ...

  • Vue2.x的虚拟DOM diff原理

    1.什么是虚拟dom? (1)什么是虚拟DOM?vdom可以看作是一个使用javascript模拟了DOM结构的树...

  • vue-diff算法

    虚拟DOM(Virtual Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了...

  • 第五天

    1、详述虚拟DOM中的diff算法? 1.用js对象模拟DOM树 2.比较两颗虚拟dom树的差异 3....

  • 初步理解Virtual DOM和diff算法

    一、virtual dom是什么?它为什么会出现? 1、是什么? virtual dom即 虚拟dom 用js模拟...

网友评论

      本文标题:简单模拟虚拟dom

      本文链接:https://www.haomeiwen.com/subject/juyqxhtx.html