发布于 2026-01-06 0 阅读
0

解释并创建了一个简单的虚拟 DOM。抱歉我的英语不好,所以写得不太好。

从零开始解释并创建了一个简单的虚拟 DOM。

抱歉我的英语写得这么差,我不会说英语。

当我第一次听说虚拟DOM时,我很好奇它是如何工作的,以及如何创建自己的虚拟DOM。经过一番研究和实践后,我将展示我创建的虚拟DOM。

什么是 dom?

文档对象模型 (DOM) 是一种以结构化层次方式表示网页的方法,使程序员和用户能够更轻松地浏览文档。借助 DOM,我们可以使用文档对象提供的命令或方法轻松访问和操作标签、ID、类、属性或元素。

为什么称之为对象模型?

文档是使用对象建模的,该模型不仅包括文档的结构,还包括文档的行为以及构成文档的对象,例如 HTML 中带有属性的标签元素。

DOM结构:

DOM 可以被视为一棵树或一片森林(多棵树)。术语“结构模型”有时用来描述文档的树状表示。DOM 结构模型的一个重要特性是结构同构:如果使用任意两种 DOM 实现来创建同一文档的表示,它们将创建相同的结构模型,包含完全相同的对象和关系。

DOM树

更多信息

什么是虚拟DOM?

虚拟 DOM 是对象中真实 DOM 元素的内存表示。例如:

const myButton = {
    tagName: 'button',
    attrs: {
        id: 'btn',
        class: 'save-btn'
    },
    children: ['save']
};
Enter fullscreen mode Exit fullscreen mode

HTML 等效项


  <button id="btn" class="save-btn">save</button>

Enter fullscreen mode Exit fullscreen mode

了解了这些之后,我们开始吧😊

我们需要一个函数来创建一个表示元素的对象并返回该对象。

// createElement.js

function createElement(tagName, { attrs = {}, children = [] } = {}){

    return {
        tagName,
        attrs,
        children
    }
}

export default createElement;
Enter fullscreen mode Exit fullscreen mode

现在我们需要创建一个函数来渲染元素。

// render.js

function render({ tagName, attrs = {}, children = [] }){
    let element = document.createElement(tagName);
        // insert all children elements
        children.forEach( child =>  {
            if (typeof child === 'string'){
               // if the children is a kind of string create a text Node object
                element.appendChild(document.createTextNode(child));
            }
            else {
                // repeat the process with the children elements
                element.appendChild(render(child));
                }
            });
      // if it has attributes it adds them to the element
    if (Object.keys(attrs).length){
        for (const [key, value] of Object.entries(attrs)) {
            element.setAttribute(key, value);
        }
    }

    return element;
};

export default render;
Enter fullscreen mode Exit fullscreen mode

然后创建一个函数将该元素插入到 DOM 中。

// insert.js

function insertElement(element, domElement){
    domElement.replaceWith(element);
    return element;
}

export default insertElement;
Enter fullscreen mode Exit fullscreen mode

现在我们有了工具,让我们来试试吧!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>my vDOM</title>
</head>
<body>
    <div id="root">
    </div>
    <script src="./main.js" type="module"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
// main.js

import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';

let myVirtualElement = createElement("div", {
  attrs: { id: "container" },
  children: [
    createElement("p", {
      attrs: { id: "text" },
      children: ["hello world"],
    }),
  ]
});

let element = render(myVirtualElement);
let rootElemet = insertElement(element, document.querySelector('#root'));

Enter fullscreen mode Exit fullscreen mode

这段代码可以在任何 Web 服务器上运行,我是在 VS Code 中用 Live Server 运行的。

替代文字

我们成功了!🥳

现在我们可以让它更有趣,采用Jason Yu这篇文章中创建的算法来区分虚拟元素

// diff.js

import render from './render.js';

const zip = (xs, ys) => {
  const zipped = [];
  for (let i = 0; i < Math.max(xs.length, ys.length); i++) {
    zipped.push([xs[i], ys[i]]);
  }
  return zipped;
};

const diffAttrs = (oldAttrs, newAttrs) => {
  const patches = [];

  // set new attributes
  for (const [k, v] of Object.entries(newAttrs)) {
    patches.push($node => {
      $node.setAttribute(k, v);
      return $node;
    });
  }

  // remove old attributes
  for (const k in oldAttrs) {
    if (!(k in newAttrs)) {
      patches.push($node => {
        $node.removeAttribute(k);
        return $node;
      });
    }
  }

  return $node => {
    for (const patch of patches) {
      patch($node);
    }
  };
};

const diffChildren = (oldVChildren, newVChildren) => {
  const childPatches = [];
  oldVChildren.forEach((oldVChild, i) => {
    childPatches.push(diff(oldVChild, newVChildren[i]));
  });

  const additionalPatches = [];
  for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
    additionalPatches.push($node => {
      $node.appendChild(render(additionalVChild));
      return $node;
    });
  }

  return $parent => {
    for (const [patch, child] of zip(childPatches, $parent.childNodes)) {
      patch(child);
    }

    for (const patch of additionalPatches) {
      patch($parent);
    }

    return $parent;
  };
};

const diff = (vOldNode, vNewNode) => {
  if (vNewNode === undefined) {
    return $node => {
      $node.remove();
      return undefined;
    };
  }

  if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
    if (vOldNode !== vNewNode) {
      return $node => {
        const $newNode = render(vNewNode);
        $node.replaceWith($newNode);
        return $newNode;
      };
    } else {
      return $node => undefined;
    }
  }

  if (vOldNode.tagName !== vNewNode.tagName) {
    return $node => {
      const $newNode = render(vNewNode);
      $node.replaceWith($newNode);
      return $newNode;
    };
  }

  const patchAttrs = diffAttrs(vOldNode.attrs, vNewNode.attrs);
  const patchChildren = diffChildren(vOldNode.children, vNewNode.children);

  return $node => {
    patchAttrs($node);
    patchChildren($node);
    return $node;
  };
};

export default diff;
Enter fullscreen mode Exit fullscreen mode

现在我们来修改 main.js

// main.js

import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';
import diff from './diff.js';

let myElement = createElement('div', {
    attrs: { class: 'container'},
    children: [createElement('img', {
        attrs: { id: 'img', src: 'https://i.picsum.photos/id/1/200/300.jpg' },
        children: []
    })]
})


let element = render(myElement);
let rootElemet = insertElement(element, document.querySelector('#root'));

let count = 0;

setInterval(()=> {
    count += 1;
    let myVirtualElemet = createElement('div', {
        attrs: { class: 'img'},
        children: [createElement('img', {
            attrs: { id: 'img', src: `https://i.picsum.photos/id/${count}/200/300.jpg` },
            children: []
        })]
    })

    const patch = diff(myElement, myVirtualElemet);

    rootElemet = patch(rootElemet);


    myElement = myVirtualElemet;

}, 1000);

Enter fullscreen mode Exit fullscreen mode

运行它🤞

gif

我们成功了!🥳

我们每秒都会用新的 id 更改链接中的 src 属性,以便更新 DOM 并应用更改。

抱歉我的英语写得这么差,我不会说英语。

文章来源:https://dev.to/buttercubz/explained-and-created-a-simple-virtual-dom-from-scratch-5765