<style>
#main {
border: 1px solid #666;
padding: 5px;
}
.red {
color: red;
}
</style>
<div id="main"> </div>
// 01思路: 虚拟dom是个对象,上面是对标签数据的描述
// tag:标签类型 props:标签属性 child子元素(字符串|VnDOm)
const VnDOm = (tag, props, child = []) => ({ tag, props, child });
// 02数据: 生成虚拟DOM树
const createVnDOm = (list) => {
const childData = list.map(item => VnDOm('li', { class: 'red', title: item }, item));
const ulData = VnDOm('ul', { class: 'content' }, childData);
return ulData;
};
const createVnDOm1 = (list) => {
const childData = list.map((item, index) => VnDOm('li', { class: 'red', title: item }, item));
const ulData = VnDOm('ul', { class: 'changeContent' }, childData);
return ulData;
};
// 03真实DOM : 将VoDOM挂载根DOM
// root:根节点 resDOM:虚拟DOM
const mountVnDOm = (root, resDOM) => {
if (typeof resDOM == "string") {
return root.innerHTML = resDOM;
}
// 标签元素
const element = document.createElement(resDOM.tag);
// 将真实DOM绑定在resDOM,这样在比对数据的时候,可以方便的更新该节点数据对应的真实DOM
resDOM.element = element;
// 挂载属性
const domAttrs = resDOM.props;
if (domAttrs) {
for (const [key, val] of Object.entries(domAttrs)) {
element.setAttribute(key, val);
};
}
// 遍历子元素
const childData = resDOM.child;
if (typeof childData == "string") {
element.innerHTML = childData;
} else {
childData.forEach(item => {
mountVnDOm(element, item);
});
};
// 挂载子元素
if (typeof root == "string") {
const rootDOM = document.querySelector(root); // 根元素
rootDOM.appendChild(element);
} else {
root.appendChild(element)
}
};
// 04数据比对: 新的VnDom和旧VnDom找出发生变化的数据 => 映射到视图
const patchFn = (oldVnDOm, newVnDOm) => {
// 将虚拟DOM对应的真实元素,都指向同一个,便于后面修改
const element = (newVnDOm.element = oldVnDOm.element);
// 跟节点标签不同,删除根节点DOM,重新渲染新的数据
if (oldVnDOm.tag !== newVnDOm.tag) {
const root = element.parentNode;
mountVnDOm(root, newVnDOm);
root.removeChild(element);
} else { // 比较每个子节点数据
const oldVnDOmChild = oldVnDOm.child;
const newVnDOmChild = newVnDOm.child;
// 更新节点属性
for (const [key, val] of Object.entries(newVnDOm.props)) {
element.setAttribute(key, val)
};
// 子节点字符串,并且内容不一样
if (typeof newVnDOmChild == "string" && (oldVnDOmChild !== newVnDOmChild)) {
element.innerHTML = newVnDOmChild;
}
// 子节点数组
if (Array.isArray(oldVnDOmChild)) {
// 数据长度
const oldSize = oldVnDOmChild.length;
const newSize = newVnDOmChild.length;
// 使用最小长度数据作为依据(长度一致返回同一个值),进行比较
const commonLen = Math.min(oldSize, newSize);
for (let i = 0; i < commonLen; i++) {
patchFn(oldVnDOmChild[i], newVnDOmChild[i])
};
// 数据不相等时候 => 处理比较之后,未处理的剩下数据
// I: oldVnDOm渲染4条数据,newVnDOm数据2条,则需要处理oldVnDOm剩余的2个数据(删除)
if (oldSize > newSize) {
oldVnDOmChild.slice(newSize).forEach(val => val.element.parentNode.removeChild(val.element))
}
// II: oldVnDOm渲染2条数据,newVnDOm数据4条,则需要处理newVnDOm剩余的2个数据(增加)
if (newSize > oldSize) {
// element => 虚拟DOM数据上由于没有绑定element,则此处的element为上级DOM
newVnDOmChild.slice(oldSize).forEach(val => {
mountVnDOm(element, val);
})
}
}
}
};
// I: 第一次直接渲染数据
const VnDOmData1 = createVnDOm(['apple', 'pea', 'age']);
mountVnDOm('#main', VnDOmData1);
// II: 数据更新 => 需要patchFn函数找出需要更新的函数
setTimeout(() => {
const VnDOmData2 = createVnDOm1(['apple', '桃子', '名字', '变化的6']);
patchFn(VnDOmData1, VnDOmData2);
}, 1000);