简单实现虚拟dom

121 阅读4分钟

虚拟 dom 的简易版本

模拟虚拟 dom 的实现

概述

  1. vnode.js 导出一个虚拟 dom 的构造函数
  2. createElement.js(h 函数) 导出一个方法,该方法能将传入的参数转化为虚拟 dom 并返回
  3. createDom.js 导出一个方法,该方法接收一个虚拟 dom,然后根据这个虚拟 dom 返回真实 dom
  4. isSameVnode.js 用于判断两个虚拟 dom 是否相同(通过 key 与标签判断)
  5. patch.js 导出一个方法比较新旧两数的根节点,更具情况更新真实 dom,最后返回新树的根节点
  6. patchVnode.js 导出一个方法来对比两个虚拟 dom 并根据情况更新真实 dom
  7. updateChildren.js 导出一个方法,当两个虚拟节点相同且都有子元素时调用该方法,对两数的子元素进行比对通过 diff 算法找到最小更新量并更新真实 dom

vnode.js

export default class Vnode {
  /**
   *
   * @param {String} tag 标签
   * @param {Object} data 数据
   * @param {Vnode[]} children 子元素
   * @param {String} text 文本内容
   * @param {Dom} elm 真实dom
   */
  constructor(tag, data, children, text, elm) {
    this.tag = tag;
    this.data = data || {};
    this.children = children || [];
    this.text = text;
    this.elm = elm;
  }
}

createElement.js(h 函数)

import Vnode from "./vnode.js";

/**
 *
 * @param {String} tag 标签名
 * @param {Object} data
 * @param {vnode,String,Array} child 可以是vnode可以是字符串也可以是一个数组
 */
export default function (tag, data, child) {
  if (arguments.length !== 3) {
    return new Error("请传入正确的参数");
  }
  if (typeof child === "string" || typeof child === "number") {
    return new Vnode(tag, data, undefined, child, undefined);
  } else if (Array.isArray(child)) {
    let children = [];
    child.forEach((item) => {
      if (item instanceof Vnode) children.push(item);
      else throw new Error("第三个参数为数组时,每一项都必须是h()函数");
    });
    return new Vnode(tag, data, children, undefined, undefined);
  } else if (child instanceof Vnode) {
    return new Vnode(tag, data, [child], undefined, undefined);
  }
}

createDom.js

// 根据虚拟dom数创建真实dom
export default function createDom(vnode) {
  let container = document.createElement(vnode.tag);
  if (vnode.text) {
    container.innerText = vnode.text;
  }
  // 添加属性
  if (vnode.data) {
    for (const key in vnode.data) {
      if (Object.hasOwnProperty.call(vnode.data, key)) {
        const value = vnode.data[key];
        container[key] = value;
      }
    }
  }
  // 递归调用
  if (vnode.children.length !== 0) {
    vnode.children.forEach((child) => {
      container.appendChild(createDom(child));
    });
  }
  vnode.elm = container;
  return container;
}

isSameVnode.js

export default function (vnode1, vnode2) {
  // 判断标签以及key值是否相同
  return (
    (vnode1.data ? vnode1.data.key : undefined) ===
      (vnode2.data ? vnode2.data.key : undefined) && vnode1.tag === vnode2.tag
  );
}

patch.js

import isSameVnode from "./isSameVnode.js";
import Vnode from "./vnode.js";
import createDom from "./createDom.js";
import patchVnode from "./patchVnode.js";

function emptyNodeAt(elm) {
  return new Vnode(
    elm.tagName.toLowerCase(),
    undefined,
    undefined,
    undefined,
    elm
  );
}

export default function (newVnode, oldVnode) {
  // 如果传入的旧树并不是vonde证明是首次加载
  if (!(oldVnode instanceof Vnode)) {
    oldVnode = emptyNodeAt(oldVnode);
  }
  // 如果两个虚拟dom相同
  if (isSameVnode(newVnode, oldVnode)) {
    // 更新
    patchVnode(newVnode, oldVnode);
  } else {
    // 直接删除就树换上新树
    let newDom = createDom(newVnode);
    let parent = oldVnode.elm.parentElement;
    parent.removeChild(oldVnode.elm);
    parent.appendChild(newDom);
  }
  newVnode.elm = oldVnode.elm;
  return newVnode;
}

patchVnode.js

import createDom from "./createDom.js";
import unpdateChildren from "./updateChildren.js";

export default function patchVnode(newVnode, oldVnode) {
  // 注意:h函数定义时就决定了一个虚拟dom要么有text要么有children子元素

  // 同一虚拟节点直接return
  if (newVnode === oldVnode) return;

  // 1.新结点有文本,而旧节点则是有一堆子元素
  if (newVnode.text && oldVnode.children.length > 0) {
    oldVnode.elm.innerHTML = "";
    oldVnode.elm.innerText = newVnode.text;
  }

  // 新结点有文本,旧节点也是文本
  if (newVnode.text && oldVnode.text) {
    oldVnode.elm.innerText = newVnode.text;
  }

  // 更新data(也就是更新属性)
  // 获取两颗数所有的属性
  const allData = Object.assign({}, oldVnode.data, newVnode.data);
  for (const key in allData) {
    if (Object.hasOwnProperty.call(allData, key)) {
      const element = allData[key];
      // 如果这个属性旧树有而新树没有,移除该属性
      // 如果这个属性旧树没有或者新旧两树不相等,则对该属性直接赋值
      if (!newVnode.data[key]) {
        oldVnode.elm.removeAttribute(key);
      } else if (
        !oldVnode.data[key] ||
        oldVnode.data[key] !== newVnode.data[key]
      ) {
        oldVnode.elm[key] = element;
      }
    }
  }

  // 如果新树有子元素
  if (newVnode.children.length > 0) {
    // 如果旧树没有子元素
    if (oldVnode.children.length <= 0) {
      oldVnode.elm.innerHTML = "";
      newVnode.children.forEach((child) => {
        oldVnode.elm.appendChild(createDom(child));
      });
    } else {
      // 新旧两数都有children子元素的情况
      unpdateChildren(oldVnode.elm, newVnode.children, oldVnode.children);
    }
  }
}

updateChildren.js

import createDom from "./createDom.js";
import isSameVnode from "./isSameVnode.js";
import patchVnode from "./patchVnode.js";

/**
 *
 * @param {Dom} parent 父节点
 * @param {Vnode[]} newCh 新树子元素
 * @param {Vnode[]} oldCh 旧树子元素
 */
export default function unpdateChildren(parent, newCh, oldCh) {
  // 旧前,旧后,新前,新后
  let oldStart = 0;
  let newStart = 0;
  let oldEnd = oldCh.length - 1;
  let newEnd = newCh.length - 1;

  let keyMap = null;
  while (oldStart <= oldEnd && newStart <= newEnd) {
    console.log("diff start");
    if (isSameVnode(newCh[newStart], oldCh[oldStart])) {
      // 前前相等
      patchVnode(newCh[newStart], oldCh[oldStart]);
      newStart++;
      oldStart++;
    } else if (isSameVnode(newCh[newEnd], oldCh[oldEnd])) {
      // 后后相等
      patchVnode(newCh[newEnd], oldCh[oldEnd]);
      newEnd--;
      oldEnd--;
    } else if (isSameVnode(newCh[newStart], oldCh[oldEnd])) {
      // 旧后 新前
      patchVnode(newCh[newStart], oldCh[oldEnd]);
      // 移动节点
      // 将当前旧后节点移动到当前旧前的前面
      parent.insertBefore(oldCh[oldEnd].elm, oldCh[oldStart].elm);
      newStart++;
      oldEnd--;
    } else if (isSameVnode(newCh[newEnd], oldCh[oldStart])) {
      // 旧前,新后
      patchVnode(isSameVnode(newCh[newEnd], oldCh[oldStart]));
      // 移动节点
      // 将当前旧前移动到当前旧后的后面
      parent.insertBefore(oldCh[oldStart].elm, oldCh[oldEnd].elm.nextSibling);
      newEnd--;
      oldStart++;
    } else {
      // 初始化keymap
      if (!keyMap) {
        keyMap = new Map();
        for (let i = oldStart; i <= oldEnd; i++) {
          const key = oldCh[i].data?.key;
          if (key) {
            keyMap.set(key, i);
          }
        }
      }
      const newKey = newCh[newStart].data?.key;
      const index = keyMap.get(newKey) || undefined;
      // 如果找到key值相同的
      if (index) {
        // 要移动的节点
        const moveEle = oldCh[index];
        patchVnode(newCh[newStart], moveEle);
        // 将该结点从旧树去掉
        oldCh.splice(index, 1);
        // 将要移动的节点插入旧前的前面
        parent.insertBefore(moveEle.elm, oldCh[oldStart].elm);
      } else {
        // 没有的情况下直接创建新的真实dom并插入到,旧前节点的前面
        parent.insertBefore(createDom(newCh[newStart], oldCh[oldStart].elm));
      }
      newStart++;
    }
    // 如果还有旧节点直接删除
    if (oldStart <= oldEnd) {
      for (let i = oldStart; i <= oldEnd; i++) {
        parent.removeChild(oldCh[i].elm);
      }
    }
    // 还有新结点直接添加
    if (newStart <= newEnd) {
      for (let i = newStart; i <= newEnd; i++) {
        parent.appendChild(createDom(newCh[i]));
      }
    }
  }
}