虚拟DOM

虚拟DOM是现代前端框架的核心技术之一,它通过在内存中维护DOM的抽象表示来提高渲染效率。本章将介绍虚拟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, '内容')
);

渲染虚拟DOM

创建真实DOM

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);
        }
    };
}

Diff算法

比较策略

const diffStrategy = {
    sameType: '同类型节点:比较属性和子节点',
    differentType: '不同类型节点:替换整个节点',
    textNode: '文本节点:比较文本内容',
    listDiff: '列表节点:使用key优化比较'
};

实现Diff

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;
}

列表Diff

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;
}

Patch应用

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

下一步

下一章将探讨 响应式原理,学习数据响应式系统的实现原理。