虚拟DOM是现代前端框架的核心技术之一,它通过在内存中维护DOM的抽象表示来提高渲染效率。本章将介绍虚拟DOM的原理和实现。
const virtualDOMConcept = {
definition: '虚拟DOM是真实DOM的JavaScript对象表示',
advantages: [
'跨平台 - 可以渲染到不同平台',
'性能优化 - 批量更新,最小化DOM操作',
'开发体验 - 声明式编程',
'可测试 - 纯JavaScript对象'
],
workflow: [
'1. 创建虚拟DOM树',
'2. 渲染为真实DOM',
'3. 状态变化创建新虚拟DOM',
'4. Diff比较新旧虚拟DOM',
'5. Patch应用差异到真实DOM'
]
};
const vnodeExample = {
type: 'div',
props: {
id: 'container',
className: 'wrapper',
style: { color: 'red' }
},
children: [
{
type: 'h1',
props: {},
children: '标题'
},
{
type: 'p',
props: { className: 'text' },
children: '段落内容'
}
]
};
function createElement(type, props, ...children) {
return {
type,
props: props || {},
children: children
.flat(Infinity)
.map(child =>
typeof child === 'object'
? child
: { type: 'TEXT', props: {}, children: String(child) }
)
};
}
const vnode = createElement(
'div',
{ id: 'app' },
createElement('h1', null, '标题'),
createElement('p', null, '内容')
);
function render(vnode, container) {
if (typeof vnode === 'string' || typeof vnode === 'number') {
container.appendChild(document.createTextNode(vnode));
return;
}
const { type, props, children } = vnode;
if (type === 'TEXT') {
container.appendChild(document.createTextNode(children));
return;
}
const element = document.createElement(type);
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else if (key === 'ref') {
value.current = element;
} else {
element.setAttribute(key, value);
}
});
children.forEach(child => render(child, element));
container.appendChild(element);
return element;
}
function createRoot(container) {
return {
render(vnode) {
container.innerHTML = '';
render(vnode, container);
}
};
}
const diffStrategy = {
sameType: '同类型节点:比较属性和子节点',
differentType: '不同类型节点:替换整个节点',
textNode: '文本节点:比较文本内容',
listDiff: '列表节点:使用key优化比较'
};
function diff(oldVNode, newVNode) {
if (!oldVNode) {
return { type: 'CREATE', newVNode };
}
if (!newVNode) {
return { type: 'REMOVE' };
}
if (isTextNode(oldVNode) && isTextNode(newVNode)) {
if (oldVNode.children !== newVNode.children) {
return { type: 'TEXT', content: newVNode.children };
}
return null;
}
if (oldVNode.type !== newVNode.type) {
return { type: 'REPLACE', newVNode };
}
const propsPatches = diffProps(oldVNode.props, newVNode.props);
const childrenPatches = diffChildren(oldVNode.children, newVNode.children);
if (propsPatches || childrenPatches.length > 0) {
return {
type: 'UPDATE',
props: propsPatches,
children: childrenPatches
};
}
return null;
}
function isTextNode(vnode) {
return vnode.type === 'TEXT' || typeof vnode === 'string';
}
function diffProps(oldProps, newProps) {
const patches = {};
let hasChanges = false;
for (const [key, value] of Object.entries(newProps)) {
if (oldProps[key] !== value) {
patches[key] = value;
hasChanges = true;
}
}
for (const key of Object.keys(oldProps)) {
if (!(key in newProps)) {
patches[key] = null;
hasChanges = true;
}
}
return hasChanges ? patches : null;
}
function diffChildren(oldChildren, newChildren) {
const patches = [];
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
patches.push({
index: i,
patch: diff(oldChildren[i], newChildren[i])
});
}
return patches;
}
function diffList(oldChildren, newChildren) {
const oldMap = new Map();
const newMap = new Map();
oldChildren.forEach((child, index) => {
const key = child.props?.key ?? index;
oldMap.set(key, { child, index });
});
newChildren.forEach((child, index) => {
const key = child.props?.key ?? index;
newMap.set(key, { child, index });
});
const patches = [];
const moves = [];
newChildren.forEach((child, newIndex) => {
const key = child.props?.key ?? newIndex;
const oldEntry = oldMap.get(key);
if (oldEntry) {
const patch = diff(oldEntry.child, child);
if (patch) {
patches.push({ type: 'UPDATE', oldIndex: oldEntry.index, newIndex, patch });
}
} else {
patches.push({ type: 'CREATE', newIndex, newVNode: child });
}
});
oldChildren.forEach((child, oldIndex) => {
const key = child.props?.key ?? oldIndex;
if (!newMap.has(key)) {
patches.push({ type: 'REMOVE', oldIndex });
}
});
return patches;
}
function patch(parent, patch, index = 0) {
if (!patch) return;
const element = parent.childNodes[index];
switch (patch.type) {
case 'CREATE':
render(patch.newVNode, parent);
break;
case 'REMOVE':
if (element) {
parent.removeChild(element);
}
break;
case 'REPLACE':
if (element) {
const newElement = render(patch.newVNode, document.createDocumentFragment());
parent.replaceChild(newElement, element);
}
break;
case 'TEXT':
if (element) {
element.textContent = patch.content;
}
break;
case 'UPDATE':
if (element) {
patchProps(element, patch.props);
patch.children.forEach(childPatch => {
patch(element, childPatch.patch, childPatch.index);
});
}
break;
}
}
function patchProps(element, propsPatch) {
if (!propsPatch) return;
for (const [key, value] of Object.entries(propsPatch)) {
if (value === null) {
element.removeAttribute(key);
} else if (key === 'className') {
element.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value);
} else {
element.setAttribute(key, value);
}
}
}
class MiniFramework {
constructor(container) {
this.container = container;
this.currentVNode = null;
this.component = null;
}
render(vnode) {
if (!this.currentVNode) {
this.currentVNode = vnode;
render(vnode, this.container);
} else {
const patches = diff(this.currentVNode, vnode);
patch(this.container, patches);
this.currentVNode = vnode;
}
}
createElement(type, props, ...children) {
return createElement(type, props, ...children);
}
}
function useState(initialValue) {
let value = initialValue;
const listeners = [];
function getValue() {
return value;
}
function setValue(newValue) {
value = typeof newValue === 'function' ? newValue(value) : newValue;
listeners.forEach(listener => listener(value));
}
function subscribe(listener) {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
if (index > -1) listeners.splice(index, 1);
};
}
return [getValue, setValue, subscribe];
}
function useEffect(callback, deps) {
let prevDeps = null;
function run() {
const hasChanged = !prevDeps ||
deps.some((dep, i) => dep !== prevDeps[i]);
if (hasChanged) {
callback();
prevDeps = deps;
}
}
return run;
}
⚡ 虚拟DOM性能
虚拟DOM并不总是比直接操作DOM快:
- 简单操作:直接DOM更快
- 复杂更新:虚拟DOM更高效
- 批量更新:虚拟DOM优势明显
虚拟DOM的价值在于开发体验和可维护性
🔑 Key的重要性
列表渲染时使用key:
- 帮助识别节点
- 减少不必要的DOM操作
- 保持组件状态
- 不要使用索引作为key
下一章将探讨 响应式原理,学习数据响应式系统的实现原理。