美文网首页
简单实现一个Virtual DOM

简单实现一个Virtual DOM

作者: 欢欣的膜笛 | 来源:发表于2021-05-16 15:26 被阅读0次

思路

  1. 定义一个类,用来创建 DOM 元素(element.js),含标签名、节点属性(包含样式、属性、事件)、子节点、标识 id;
  2. 比较新旧 DOM 树的差异(diff.js);
  3. 将差异的部分渲染到 DOM 树即只渲染变化了的部分(patch.js)

实现

  1. 用 javascript 对象表示 DOM 结构:element.js
class Element {
    constructor(tagName, ...args) {
        this.tagName = tagName;
        // 判断下面还有没有子元素
        if(Array.isArray(args[0])) {
            this.props = {};
            this.children = args[0];
        } else {
            this.props = args[0];
            this.children = args[1];
        }
        this.key = this.props.key || void 0;
    }
    render() {
        // 创建一个元素
        const $dom = document.createElement(this.tagName);
        // 给元素加上所有的属性
        for(const proKey in this.props) {
            $dom.setAttribute(proKey, this.props[proKey]);
        }
        // 如果存在子节点
        if(this.children) {
            this.children.forEach(child => {
                // 如果子元素还包含子元素,则递归
                if(child instanceof Element) {
                    $dom.appendChild(child.render());
                } else {
                    $dom.appendChild(document.createTextNode(child))
                }
            });
        }
        return $dom;
    }
};
export default Element;
  1. 比较新旧 DOM树的差异:深度优先遍历,记录差异

    深度优先搜索(Depth First Search)是沿着树的深度遍历树的节点,尽可能深的搜索树的分支;广度优先搜索(Breadth First Search),又叫宽度优先搜索或横向优先搜索,是从根结点开始沿着树的宽度搜索遍历。

    vue 和 react 的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设:

    • 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
    • 同一层级的一组节点,他们可以通过唯一的id进行区分。

    比较过程:

    • 比较属性的变化:
      遍历旧的属性,找到被删除和修改的情况:新属性中不存在,旧属性存在,属性被删除;新旧属性中都存在,但是值不同,属性值被修改
      遍历新元素的属性,找到添加的属性
    • 比较子元素的变化
    • 比较 innerHTML 的变化

diff.js

import patch from './patch';

function diff(oldTree, newTree) {
    const patches = {};
    const index = {
        value: 0,
    }
    dfsWalk(oldTree, newTree, index, patches);
    return patches;
}

// 比较innerHTML的变化
function dfsWalk(oldNode, newNode, index, patches) {
    const currentIndex = index.value;
    const currentIndexPatches = [];
    if(newNode === undefined) {
        // 节点被移除
        currentIndexPatches.push({
            type: patch.NODE_DELETE,
        })
    } else if(typeof oldNode === 'string' && typeof newNode === 'string') {
        // 文本节点被修改
        if(oldNode !== newNode) {
            currentIndexPatches.push({
                type: patch.NODE_TEXT_MODIFY,
                value: newNode,
            })
        }
    } else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
        // 同时根据 tagName 和 key 来进行对比
        diffProps(oldNode.props, newNode.props, index, currentIndexPatches);
        diffChildren(oldNode.children, newNode.children, index, currentIndexPatches, patches);
    } else {
        currentIndexPatches.push({
            type: patch.NODE_REPLACE,
            value: newNode,
        })
    }
    if(currentIndexPatches.length > 0) {
        patches[currentIndex] = currentIndexPatches;
    }
}

// 比较属性的变化
function diffProps(oldProps, newProps, index, currentIndexPatches) {
    // 遍历旧的属性,找到被删除和修改的情况
    for (const propKey in oldProps) {
        // 新属性中不存在,旧属性存在:属性被删除
        if (!newProps.hasOwnProperty(propKey)) {
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_DELETE,
                key: propKey,
            })
        } else if (newProps[propKey] !== oldProps[propKey]) {
            // 新旧属性中都存在,但是值不同: 属性被修改
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_MODIFY,
                key: propKey,
                alue: newProps[propKey],
            })
        }
    }

    // 遍历新元素,找到添加的部分
    for (const propKey in newProps) {
        // 旧属性中不存在,新属性中存在: 添加属性
        if (!oldProps.hasOwnProperty(propKey)) {
            currentIndexPatches.push({
                type: patch.NODE_ATTRIBUTE_ADD,
                key: propKey,
                value: newProps[propKey]
            })
        }
    }
}

// 顺序比较子元素的变化
function diffChildren(oldChildren, newChildren, index, currentIndexPatches, patches) {
    const currentIndex = index.value;
    if (oldChildren.length < newChildren.length) {
        // 有元素被添加
        let i = 0;
        for (; i < oldChildren.length; i++) {
            index.value++;
            dfsWalk(oldChildren[i], newChildren[i], index, patches)
        }
        for (; i < newChildren.length; i++) {
            currentIndexPatches.push({
                type: patch.NODE_ADD,
                value: newChildren[i]
            })
        }
    } else {
        // 对比新旧子元素的变化
        for(let i = 0; i< oldChildren.length; i++) {
            index.value++;
            dfsWalk(oldChildren[i], newChildren[i], index, patches)
        }
    }
}

export default diff;
  1. 将差异的部分渲染到DOM树即只渲染变化了的部分
    通过深度优先遍历,记录差异 patches,最后需要根据 patches 进行 DOM 操作,paches 记录了差异的类型。

patch.js

function patch($dom, patches) {
    const index = { value: 0 }
    handleDom($dom, index, patches);
}

patch.NODE_DELETE = 'NODE_DELETE'; // 节点被删除
patch.NODE_TEXT_MODIFY = 'NODE_TEXT_MODIFY'; // 文本节点被更改
patch.NODE_REPLACE = 'NODE_REPLACE'; // 节点被替代
patch.NODE_ADD = 'NODE_ADD'; // 添加节点
patch.NODE_ATTRIBUTE_MODIFY = 'NODE_ATTRIBUTE_MODIFY'; // 更新属性
patch.NODE_ATTRIBUTE_ADD = 'NODE_ATTRIBUTE_ADD'; // 添加属性
patch.NODE_ATTRIBUTE_DELETE = 'NODE_ATTRIBUTE_DELETE'; // 删除属性

// 根据不同类型的差异对当前节点进行 DOM 操作:
function handleDom($node, index, patches, isEnd = false) {
    if (patches[index.value]) {
        patches[index.value].forEach(p => {
            switch (p.type) {
                case patch.NODE_ATTRIBUTE_MODIFY:
                    {
                        $node.setAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ATTRIBUTE_DELETE:
                    {
                        $node.removeAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ATTRIBUTE_ADD:
                    {
                        $node.setAttribute(p.key, p.value);
                        break;
                    }
                case patch.NODE_ADD:
                    {
                        $node.appendChild(p.value.render());
                        break;
                    }
                case patch.NODE_TEXT_MODIFY:
                    {
                        $node.textContent = p.value;
                        break;
                    }
                case patch.NODE_REPLACE:
                    {
                        $node.replaceWith(p.value.render());
                        break;
                    }
                case patch.NODE_DELETE:
                    {
                        $node.remove();
                        break;
                    }
                default:
                    {
                        console.log(p);
                    }

            }
        });
    }
    if (isEnd) {
        return;
    }
    if ($node.children.length > 0) {
        for (let i = 0; i < $node.children.length; i++) {
            index.value++;
            handleDom($node.children[i], index, patches);
        }
    } else {
        index.value++;
        handleDom($node, index, patches, true);
    }
};

export default patch;
  1. 使用
import Element from './element';
import diff from './diff';
import patch from './patch';
// 1. 构建虚拟 DOM
const tree = new Element('div', { classname: 'div' }, [
    new Element('h1', { style: 'color: red;' }, ['Hello, This is my Vdom library']),
    new Element('ul', [
        new Element('li', ['1111']),
        new Element('li', ['2222']),
    ])
]);

// 2. 通过虚拟 DOM 构建真正的 DOM
const $dom = tree.render();
const $app = document.querySelector('#app');
$app.replaceWith($dom);

// 3. 生成新的虚拟 DOM
const newTree = new Element('div', { id: 'div1' }, [
    new Element('h1', { style: 'color: red;' }, ['Hello, This is my vdom library111']),
    new Element('p', { style: 'color: blue;' }, ['extra text']),
    new Element('ul', [
        new Element('li', ['1111']),
        new Element('li', ['5555']),
        new Element('li', ['333']),
    ])
]);

setTimeout(() => {
    // 4. 比较新旧虚拟 DOM 树的差异
    const patches = diff(tree, newTree);

    // 5. 根据变化了的部分去更新 DOM
    patch($dom, patches);
}, 2000)

相关文章

网友评论

      本文标题:简单实现一个Virtual DOM

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