DEV Community

Mavis
Mavis

Posted on

React Virtual DOM and DOM Diff

Virtual DOM works by using JS objects to simulate the nodes of the DOM. At the early stage of building React, Facebook introduced virtual DOM in consideration of factors such as improving code abstraction ability, avoiding artificial DOM operation and reducing the overall risk of code.

How it works?

The process likes that use createElement to build virtual dom, and it return a plain object that describes its tag type, props properties, children, and so on. These Plain objects form a virtual DOM tree through a tree structure.
 
render function converts virtual dom into real dom, and mount real dom, like document.getElementById("root").appendChild(realDom).
 
When the status changes, There's diff functions to compare the difference of the virtual DOM tree before and after the change. Let's have a look below two graphs. There's four different places.
A pie chart showing 40% responded "Yes", 50% responded "No" and 10% responded "Not sure"
A pie chart showing 40% responded "Yes", 50% responded "No" and 10% responded "Not sure"
The diff function returns an object, includes all different nodes information, we can call it patches.
 
doPatch function bases on patches objects, will patch one by one on real dom.

Code snap examples

How createElement looks like?
class Element {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}

function createElement(type, props, children) {
    return new Element(type, props, children);
}

Enter fullscreen mode Exit fullscreen mode

More definitions about React.createElement

createElement – React

The library for web and native user interfaces

react.dev
How virtual dom looks like?
const oldVDom = createElement(
    "ul",
    {
        class: "ul",
        style: "width: 400px; height: 400px; background-color: pink;",
    },
    [
        createElement(
            "li",
            {
                class: "li-wrap",
                "data-index": 0,
            },
            [
                createElement(
                    "p",
                    {
                        class: "text",
                    },
                    ["The first tag"]
                ),
            ]
        ),
        createElement(
            "li",
            {
                class: "li-wrap",
                "data-index": 1,
            },
            [
                createElement(
                    "p",
                    {
                        class: "text",
                    },
                    [
                        createElement(
                            "span",
                            {
                                class: "span",
                            },
                            ["The second li tag, will be removed later on."]
                        ),
                    ]
                ),
            ]
        ),
        createElement(
            "li",
            {
                class: "li-wrap",
                "data-index": 2,
            },
            ["The third tag"]
        ),
    ]
);
Enter fullscreen mode Exit fullscreen mode
How real dom looks like?

A pie chart showing 40% responded "Yes", 50% responded "No" and 10% responded "Not sure"

How render function looks like?
function render(vDom) {
    // destructure virtual dom
    const { type, props, children } = vDom;
    // transform type
    const el = document.createElement(type);
    // transform all props
    for (let key in props) {
        // transform different attributes
        setAttrs(el, key, props[key]);
    }

    children.map((c) => {
        // recursive, if child is Element, then call render function again
        // break condition is if child is not Element
        c = c instanceof Element ? render(c) : document.createTextNode(c);
        el.appendChild(c);
    });

    return el;
}
Enter fullscreen mode Exit fullscreen mode
How to mount real dom?
function renderDom(rDom, rootEl) {
    rootEl.appendChild(rDom);
}
renderDom(rDom, document.getElementById("root"));

Enter fullscreen mode Exit fullscreen mode
How patches look like?
const patches = {
    0: [
        {
            type: "ATTR",
            attrs: {
                class: "ul-wrap",
            },
        },
    ],
    3: [
        {
            type: "TEXT",
            text: "Update the first tag",
        },
    ],
    6: [
        {
            type: "REMOVE",
            index: 6,
        },
    ],
    7: [
        {
            type: "REPLACE",
            newNode: newNode,
        },
    ],
};

Enter fullscreen mode Exit fullscreen mode
How domDiff look like?
let patches = {};
let vnIndex = 0;

function domDiff(oldVDom, newVDom) {
    let index = 0;
    vNodeWalk(oldVDom, newVDom, index);
    return patches;
}

function vNodeWalk(oldNode, newNode, index) {
    let vnPatch = [];

    /**
     * node can be
     *  Element {type:'', props:{}, children: []}
     *  text string
     *  null
     */
    if (!newNode) {
        // item was removed
        vnPatch.push({
            type: REMOVE,
            index,
        });
    } else if (typeof oldNode === "string" && typeof oldNode === "string") {
        // text change
        if (oldNode != newNode) {
            vnPatch.push({
                type: TEXT,
                text: newNode,
            });
        }
    } else if (oldNode.type === newNode.type) {
        const attrPatch = attrsWalk(oldNode.props, newNode.props);

        if (Object.keys(attrPatch).length > 0) {
            vnPatch.push({
                type: ATTR,
                attrs: attrPatch,
            });
        }

        childrenWalk(oldNode.children, newNode.children);
    } else {
        vnPatch.push({
            type: REPLACE,
            newNode,
        });
    }

    if (vnPatch.length > 0) {
        patches[index] = vnPatch;
    }
}

function childrenWalk(oldChildren, newChildren) {
    oldChildren.map((child, index) => {
        vNodeWalk(child, newChildren[index], ++vnIndex);
    });
}
Enter fullscreen mode Exit fullscreen mode
How doPatch looks like?
let finalPatches = {};
let rnIndex = 0;

function doPatch(rDom, patches) {
    finalPatches = patches;
    rNodeWalk(rDom);
}

function rNodeWalk(rNode) {
    const rnPatch = finalPatches[rnIndex++];
    const childNodes = rNode.childNodes;

    [...childNodes].map((child) => {
        rNodeWalk(child);
    });

    if (rnPatch) {
        patchAction(rNode, rnPatch);
    }
}

function patchAction(rNode, rnPatch) {
    rnPatch.map((patch) => {
        switch (patch.type) {
            case ATTR:
                for (let key in patch.attrs) {
                    const value = patch.attrs[key];
                    if (value) {
                        setAttrs(rNode, key, value);
                    } else {
                        rNode.removeAttribute(key);
                    }
                }
                break;
            case TEXT:
                rNode.textContent = patch.text;
                break;
            case REPLACE:
                const newNode =
                    patch.newNode instanceof Element
                        ? render(patch.newNode)
                        : document.createTextNode(patch.newNode);
                rNode.parentNode.replaceChild(newNode, rNode);
                break;
            case REMOVE:
                rNode.parentNode.removeChild(rNode);
                break;
            default:
                break;
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)