虚拟dom跟Diff算法

前端框架如 React 和 Vue 在渲染页面时采用了虚拟 DOM 和 Diff 算法的技术,以提高性能和优化用户体验。本文将介绍虚拟 DOM、Diff 算法以及如何使用 createElement、diff 和 patch 函数实现它们。

什么是虚拟 DOM

虚拟 DOM(Virtual Document Object Model)是指将 DOM 结构抽象成 JavaScript 对象树,在这个虚拟的 JavaScript 对象树上进行操作和计算,最终再将变化的部分渲染到真实的 DOM 上。虚拟 DOM 的优点是可以最小化对真实 DOM 的操作,减少浏览器的重排和重绘,提高渲染性能和用户体验。

虚拟 DOM 的基本结构如下:

code{
  type: 'div',
  props: {
    id: 'app',
    className: 'container',
    children: [
      { type: 'h1', props: { children: 'Hello World' } },
      { type: 'p', props: { children: 'This is a paragraph.' } }
    ]
  }
}

其中 type 表示标签名,props 表示属性和子元素。

什么是 Diff 算法

Diff 算法是虚拟 DOM 中用来比较两个虚拟 DOM 树之间的差异的算法。Diff 算法会逐层比较两个树的节点,找到它们的不同点,并将这些不同点标记为更新、新增或删除。

Diff 算法的基本原理是将两个虚拟 DOM 树的同一位置的节点进行比较,如果节点类型不同,则直接替换。如果节点类型相同,则比较属性和子元素是否相同。如果属性和子元素相同,则不进行任何操作;否则,更新属性和子元素。

createElement

createElement 是一个用于创建虚拟 DOM 的函数,它接收三个参数:

  • type: 表示标签名或组件名。
  • props: 表示属性,它是一个对象,包含了所有的属性和子元素。
  • children: 表示子元素,它可以是一个数组或字符串。

例如,可以通过以下代码创建一个虚拟 DOM:

 const vertualDom = createElement('div', { id: 'app' }, [
  createElement('h1', { className: 'title' }, 'Hello World'),
  createElement('p', null, 'This is a paragraph.')
])
// Element.js
// 创建节点对象
class Element {
  constructor(type, props, children) {
    this.type = type;
    this.props = props;
    this.children = children;
  }
}
//  创建虚拟dom
function createElement(type, props, children) {
  return new Element(type, props, children);
}
// 设置属性
function setAttr(el, key, value) {
  switch (key) {
    case "value":
      if (
        el.tagName.toUpperCase() === "INPUT" ||
        el.tagName.toUpperCase() === "TEXTAREA"
      ) {
        el.value = value;
      } else {
        el.setAttribute(key, value);
      }
      break;
    case "style":
      el.style.cssText = value;
      break;
    default:
      el.setAttribute(key, value);
      break;
  }
}

//  render方法可以将vnode转化为真实dom
function render(vertualDom) {
  let el = document.createElement(vertualDom.type);
  for (let key in vertualDom.props) {
    setAttr(el, key, vertualDom.props[key]);
  }
  vertualDom.children.forEach((child) => {
    child =
      child instanceof Element ? render(child) : document.createTextNode(child);
    el.appendChild(child);
  });
  return el;
}
// dom挂载
function renderDom(el, root) {
  root.appendChild(el);
}

export { createElement, render, renderDom };


diff 和 patch

diff 函数接收两个参数,分别是旧的虚拟 DOM 树和新的虚拟 DOM 树,它会返回一个表示差异的对象。patch 函数接收三个参数,分别是真实的 DOM 节点、旧的虚拟 DOM 树和表示差异的对象。patch 函数会根据差异对象来更新真实的 DOM 节点。

以下是 diff 函数的实现代码:

diff.js

let Index = 0;
// diff算法
function diff(oldTree, newTree) {
  let patches = {};
  let index = 0;
  walk(oldTree, newTree, index, patches);
  return patches;
}
// 对比新老节点的变化,将变化的内容存到补丁包里面
function walk(oldTree, newTree, index, patches) {
  let currentPatch = [];
  if (!newTree) {
    currentPatch.push({ type: "REMOVE", index });
  } else if (isString(oldTree) && isString(newTree)) {
    if (oldTree !== newTree) {
      currentPatch.push({ type: "TEXT", text: newTree });
    }
  } else if (oldTree.type === newTree.type) {
    let attrs = diffAttr(oldTree.props, newTree.props);
    console.log(attrs);
    if (Object.keys(attrs).length > 0) {
      currentPatch.push({ type: "ATTR", attrs });
    }
    diffChildren(oldTree.children, newTree.children, patches);
  } else {
    currentPatch.push({ type: "REPLACE", newTree });
  }

  if (currentPatch.length > 0) {
    console.log(index);
    patches[index] = currentPatch;
  }
}
// 判断是否为字符串,如果为字符串直接渲染
function isString(obj) {
  return Object.prototype.toString.call(obj) === "[object String]";
}
// 节点比较
function diffChildren(oldChildren, newChildren, patches) {
  oldChildren.forEach((child, idx) => {
    walk(child, newChildren[idx], ++Index, patches);
  });
}

// 节点属性设置
function diffAttr(oldAttrs, newAttrs) {
  let patch = {};
  for (let key in oldAttrs) {
    if (oldAttrs[key] !== newAttrs[key]) {
      patch[key] = newAttrs[key];
    }
  }

  for (let key in newAttrs) {
    if (!oldAttrs.hasOwnProperty(key)) {
      patch[key] = newAttrs[key];
    }
  }
  return patch;
}
patch.js

let allPatches;
let index = 0;
// diff对比完毕,使用patch进行补丁渲染
function patch(node, patches) {
  console.log(patches);
  allPatches = patches;
  walk(node);
}
// 查找是否有补丁,如果有进行替换
function walk(node) {
  let currentPatch = allPatches[index++];
  let childNodes = node.childNodes;
  childNodes.forEach((child) => walk(child));
  if (currentPatch) {
    doPatch(node, currentPatch);
  }
}
// 补丁替换
function doPatch(node, patches) {
  patches.forEach((patch) => {
    switch (patch.type) {
      case "ATTR":
        for (let key in patch.attrs) {
          let value = patch.attrs[key];
          if (value) {
            setAttr(node, key, value);
          } else {
            node.removeAttribute(key);
          }
        }
        break;
      case "TEXT":
        node.textContent = patch.text;
        break;
      case "REPLACE":
        let newNode =
          patch.newTree instanceof Element
            ? render(patch.newTree)
            : document.createTextNode(patch.newTree);
        node.parentNode.replaceChild(newNode, node);
        break;
      case "REMOVE":
        node.parentNode.removeChild(node);
        break;
      default:
        break;
    }
  });
}
// 如果补丁为属性时进行替换
function setAttr(node, key, value) {
  console.log(node, key, value);
  switch (key) {
    case "value":
      if (
        node.tagName.toUpperCase() === "INPUT" ||
        node.tagName.toUpperCase() === "TEXTAREA"
      ) {
        node.value = value;
      } else {
        node.setAttribute(key, value);
      }
      break;
    case "style":
      node.style.cssText = value;
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
}
export { patch };


在上述代码中,diff 函数通过比较旧的虚拟 DOM 树和新的虚拟 DOM 树,返回表示差异的对象。patch 函数根据差异对象来更新真实的 DOM 节点。

总结

本文介绍了虚拟 DOM、Diff 算法以及如何使用 createElement、diff 和 patch 函数实现它们。虚拟 DOM 和 Diff 算法的应用可以优化前端页面的渲染性能和用户体验,是现代前端框架的核心技术之一。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值