简单模拟虚拟dom

虚拟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后对于页面进行更改:


点击前

点击后

结尾

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

你可能感兴趣的:(简单模拟虚拟dom)