虚拟dom是现在前端非常重要的一个技术,这篇文章试着简单模拟一下虚拟dom功能。
虚拟dom比较内存中虚拟dom的差异,然后通过差异来修改html中真实的dom节点,这中间对于dom进行最小修改从而减少页面的重排与重绘。
虚拟dom类
首先就要写一个虚拟dom的类来模拟html中真实节点:
class Element {
constructor(tagName, attributes = {}, children = []) {
this.tagName = tagName;
this.attributes = attributes;
this.children = children;
}
render() {
const ele = document.createElement(this.tagName);
if (this.children && this.children.length > 0) {
this.children.forEach((childElement) => {
// 简单判断是节点还是文案
if (childElement instanceof Element) {
ele.appendChild( childElement.render());
} else if(typeof childElement === 'string') {
ele.appendChild(document.createTextNode(childElement));
}
})
}
return ele;
}
}
方便测试在创建两个工具方法:
// 构建虚拟dom节点
const element = (tagName, attributes, children) => {
return new Element(tagName, attributes, children);
};
const renderDom = (ele, dom) => {
dom.appendChild(ele);
};
测试
虚拟dom比较方法
const compareDiff = (newVirtualDom, oldVirtualDom, index, patch) => {
const diffresult = [];
if (!newVirtualDom) {
diffresult.push({
type: NODE_OPERATOR_TYPE.REMOVE_NODE,
})
} else if (typeof newVirtualDom === 'string' && typeof oldVirtualDom === 'string') {
if (oldVirtualDom !== newVirtualDom) {
diffresult.push({
type: NODE_OPERATOR_TYPE.MODIFY_TEXT,
data: newVirtualDom
})
}
} else if (newVirtualDom.tagName === oldVirtualDom.tagName) {
const diffAttributeResult = {};
// 先遍历老dom找出变化的属性值, 再遍历新dom找出新添加的属性
for (const key in oldVirtualDom) {
if (oldVirtualDom[key] !== newVirtualDom[key]) {
diffAttributeResult[key] = newVirtualDom[key];
}
}
for (const key in newVirtualDom) {
if (!oldVirtualDom.hasOwnProperty(key)) {
diffAttributeResult[key] = newVirtualDom[key];
}
}
if (Object.keys(diffAttributeResult).length > 0) {
diffresult.push({
type: NODE_OPERATOR_TYPE.MODIFY_ATTRIBUTE,
diffAttributeResult
})
}
// 在节点相同情况下遍历子节点进行比较
oldVirtualDom.children.forEach((childEle, index) => {
compareDiff(newVirtualDom.children[index], childEle, ++initialIndex, patch)
})
} else {
diffresult.push({
type: NODE_OPERATOR_TYPE.REPLACE_NODE,
newVirtualDom
})
}
if (diffresult.length > 0) {
patch[index] = diffresult;
}
};
这里只是简单的进行了判断,有些方面考虑并不周全
工具方法
const diff = (oldVirtualDom, newVirtualDom) => {
let patches = {}
// 递归树 比较后的结果放到 patches
compareDiff(oldVirtualDom, newVirtualDom, 0, patches)
return patches
}
测试
const fruitVirtualDom = element('ul', {id: 'list'}, [
element('li', {class: 'fruit'}, ['苹果']),
element('li', {class: 'fruit'}, ['橘子']),
element('li', {class: 'fruit'}, ['葡萄'])
]);
const fruitVirtualDom1 = element('ul', {id: 'list'}, [
element('li', {class: 'fruit'}, ['苹果a']),
element('div', {class: 'fruit'}, ['橘子']),
element('li', {class: 'fruit'}, ['葡萄'])
]);
const result = diff(fruitVirtualDom1, fruitVirtualDom)
console.log(result)
这里没有递归对于属性对象进行深度判断,所以多了很多diff结果。
应用patch
得到虚拟dom差异后就可以应用在html中真实dom节点上了
function doPatch(node, diffResult) {
diffResult.forEach((diff) => {
switch (diff.type) {
case NODE_OPERATOR_TYPE.REPLACE_NODE:
const newVirtualDom = diff.newVirtualDom;
let newNode = null;
if (newVirtualDom instanceof Element) {
newNode = newVirtualDom.render();
} else if (typeof newVirtualDom === 'string') {
newNode = document.createTextNode(newVirtualDom);
}
node.parentNode.replaceChild(newNode, node);
break;
case NODE_OPERATOR_TYPE.MODIFY_ATTRIBUTE:
const diffAttributeResult = diff.diffAttributeResult;
const attributes = diffAttributeResult.attributes;
for (const attr in attributes) {
if (node.nodeType === 1) {
if (attributes[attr]) {
setAttribute(node, attr, attributes[attr]);
} else {
node.removeAttribute(attr);
}
}
}
break;
case NODE_OPERATOR_TYPE.MODIFY_TEXT:
node.textContent = diff.data;
break;
case NODE_OPERATOR_TYPE.REMOVE_NODE:
node.parentNode.removeChild(node);
break;
default:
break;
}
})
}
function walk(node, walker, patches) {
const currentPatch = patches[walker.index];
node.childNodes.forEach((node, index) => {
walker.index++;
walk(node, walker, patches);
})
if (currentPatch) {
doPatch(node, currentPatch);
}
}
const patch = (node, patches) => {
const walker = {index: 0};
walk(node, walker, patches);
}
这里还有一个工具方法setAttribute,对于原生的方法加了小小封装:
const setAttribute = (node, key, value) => {
switch (key) {
case 'style':
node.style.cssText = value
break
case 'value':
let tagName = node.tagName || ''
tagName = tagName.toLowerCase()
if (
tagName === 'input' || tagName === 'textarea'
) {
node.value = value
} else {
// 如果节点不是 input 或者 textarea, 则使用 setAttribute 去设置属性
node.setAttribute(key, value)
}
break
default:
node.setAttribute(key, value)
break
}
}
最终测试
再点击button后对于页面进行更改:
结尾
这篇文章只是简单模拟一下虚拟dom的功能,还有很多问题没有考虑,大家在看的时候不要太过于纠结,理解这里边大体思想就好。