虚拟 dom 的简易版本
模拟虚拟 dom 的实现
概述
- vnode.js
导出一个虚拟
dom
的构造函数 - createElement.js(h 函数)
导出一个方法,该方法能将传入的参数转化为虚拟
dom
并返回 - createDom.js
导出一个方法,该方法接收一个虚拟
dom
,然后根据这个虚拟dom
返回真实dom
- isSameVnode.js
用于判断两个虚拟
dom
是否相同(通过 key 与标签判断) - patch.js
导出一个方法比较新旧两数的根节点,更具情况更新真实
dom
,最后返回新树的根节点 - patchVnode.js
导出一个方法来对比两个虚拟
dom
并根据情况更新真实dom
- 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]));
}
}
}
}