返回首页

说说 React 中的虚拟 DOM 的理解?

问题解析

面试官考察点:

  • 是否理解虚拟 DOM 的概念和作用
  • 是否了解虚拟 DOM 的工作原理
  • 是否理解 Diff 算法
  • 对 React 性能优化机制的理解

核心概念

什么是虚拟 DOM

虚拟 DOM(Virtual DOM)本质上是以 JavaScript 对象形式存在的对真实 DOM 的描述。它是对真实 DOM 的抽象表示,通过对象树的形式来描述 UI 结构。

// JSX
const element = (
  <div className="container">
    <h1>Hello World</h1>
    <p>Welcome to React</p>
  </div>
);

// 编译后(虚拟 DOM 对象)
const element = React.createElement(
  'div',
  { className: 'container' },
  React.createElement('h1', null, 'Hello World'),
  React.createElement('p', null, 'Welcome to React')
);

// 虚拟 DOM 对象结构
{
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello World' }
      },
      {
        type: 'p',
        props: { children: 'Welcome to React' }
      }
    ]
  }
}

为什么需要虚拟 DOM

  1. 跨平台能力:虚拟 DOM 是平台无关的,可以渲染到 Web、Native、Canvas 等不同平台
  2. 性能优化:通过 Diff 算法最小化 DOM 操作
  3. 开发体验:声明式编程,开发者只需关注状态变化

详细解答

1. 虚拟 DOM 的创建

// React.createElement 实现简化版
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === 'object' ? child : createTextElement(child)
      )
    }
  };
}

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT',
    props: {
      nodeValue: text,
      children: []
    }
  };
}

// 使用
const element = createElement(
  'div',
  { id: 'app' },
  createElement('h1', null, 'Hello')
);

2. 虚拟 DOM 转真实 DOM

// 渲染虚拟 DOM 到真实 DOM
function render(element, container) {
  const dom =
    element.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(element.type);

  // 设置属性
  Object.keys(element.props)
    .filter(key => key !== 'children')
    .forEach(name => {
      dom[name] = element.props[name];
    });

  // 递归渲染子元素
  element.props.children.forEach(child =>
    render(child, dom)
  );

  container.appendChild(dom);
}

3. Diff 算法

React 使用 Diff 算法比较新旧虚拟 DOM,找出最小变更集。

Diff 策略

// 1. 同层比较(Tree Diff)
// React 只对同一层次的节点进行比较
// 如果节点跨层级移动,React 不会尝试复用,而是删除旧节点,创建新节点

// 旧树
<div>
  <A />
</div>

// 新树
<span>
  <A />
</span>

// A 会被销毁并重新创建,因为父节点从 div 变成了 span


// 2. 组件比较(Component Diff)
// 如果是同一类型的组件,继续比较 Virtual DOM
// 如果是不同类型的组件,直接替换整个组件

// 同一类型组件
<OldComponent />  ->  <OldComponent />  // 继续比较

// 不同类型组件
<OldComponent />  ->  <NewComponent />  // 直接替换


// 3. 元素比较(Element Diff)
// 对于同一层级的子节点,通过 key 来标识

// 旧列表
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

// 新列表
<ul>
  <li key="b">B</li>
  <li key="a">A</li>
  <li key="c">C</li>
</ul>

// 通过 key 识别,只需移动 A 和 B,插入 C

Diff 算法实现简化版

function diff(oldVNode, newVNode) {
  // 1. 类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newVNode };
  }

  // 2. 文本节点
  if (newVNode.type === 'TEXT_ELEMENT') {
    if (oldVNode.props.nodeValue !== newVNode.props.nodeValue) {
      return { type: 'TEXT', content: newVNode.props.nodeValue };
    }
    return null;
  }

  // 3. 比较属性
  const patches = [];
  const oldProps = oldVNode.props;
  const newProps = newVNode.props;

  // 删除旧属性
  for (let key in oldProps) {
    if (!(key in newProps)) {
      patches.push({ type: 'REMOVE_PROP', key });
    }
  }

  // 添加/更新属性
  for (let key in newProps) {
    if (oldProps[key] !== newProps[key]) {
      patches.push({ type: 'SET_PROP', key, value: newProps[key] });
    }
  }

  // 4. 比较子节点
  const childrenPatches = diffChildren(
    oldProps.children,
    newProps.children
  );

  return { type: 'UPDATE', patches, childrenPatches };
}

function diffChildren(oldChildren, newChildren) {
  const patches = [];
  const maxLen = Math.max(oldChildren.length, newChildren.length);

  for (let i = 0; i < maxLen; i++) {
    patches.push(diff(oldChildren[i], newChildren[i]));
  }

  return patches;
}

4. Key 的作用

// 没有 key 的情况
// 旧:[A, B, C]
// 新:[B, C, D]

// 比较过程:
// A -> B (不同,更新)
// B -> C (不同,更新)
// C -> D (不同,更新)
// 删除多余的

// 有 key 的情况
// 旧:[A(key=1), B(key=2), C(key=3)]
// 新:[B(key=2), C(key=3), D(key=4)]

// 比较过程:
// 通过 key 识别,A 被删除,B 和 C 保持不变,D 被添加

5. Fiber 架构

React 16 引入了 Fiber 架构,将渲染工作拆分为小单元,支持可中断和恢复。

// Fiber 节点结构
{
  type: 'div',           // 组件类型
  key: null,             // key
  props: {},             // props
  stateNode: domNode,    // 对应的真实 DOM

  // 链表结构
  child: fiber,          // 第一个子节点
  sibling: fiber,        // 下一个兄弟节点
  return: fiber,         // 父节点

  // 副作用
  effectTag: 'UPDATE',   // 标记需要执行的操作
  nextEffect: fiber,     // 下一个需要处理的副作用
}

深入理解

虚拟 DOM vs 真实 DOM

特性 虚拟 DOM 真实 DOM
类型 JavaScript 对象 浏览器 API
操作成本
跨平台
内存占用 额外开销 正常
首次渲染 较慢(需要计算) 较快

虚拟 DOM 的优势

// 1. 批量更新
function BatchUpdate() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 多次 setState 会合并为一次 DOM 更新
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
    // 最终只触发一次 DOM 更新
  };
}

// 2. 跨平台渲染
// React DOM -> 浏览器
// React Native -> 移动端
// React Three Fiber -> 3D 场景

虚拟 DOM 的局限性

// 1. 不适合极高频更新
function HighFrequencyUpdate() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    // 高频更新会导致虚拟 DOM 计算开销
    const interval = setInterval(() => {
      setPosition(prev => ({
        x: prev.x + 1,
        y: prev.y + 1
      }));
    }, 16);

    return () => clearInterval(interval);
  }, []);

  // 这种情况下,直接操作 DOM 可能更高效
}

// 2. 简单操作可能直接操作 DOM 更快
// 如只修改一个文本节点,虚拟 DOM 的计算可能比直接修改更慢

最佳实践

1. 合理使用 Key

// 推荐:使用稳定的唯一标识
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

// 不推荐:使用索引作为 key(在列表顺序变化时会导致问题)
function BadList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.name}</li>
      ))}
    </ul>
  );
}

2. 避免不必要的渲染

// 使用 React.memo 减少不必要的虚拟 DOM 计算
const MemoComponent = React.memo(function MyComponent({ data }) {
  return <div>{data.name}</div>;
});

// 使用 useMemo 缓存计算结果
function ExpensiveComponent({ items }) {
  const processedItems = useMemo(() => {
    return items.map(item => expensiveOperation(item));
  }, [items]);

  return <List items={processedItems} />;
}

3. 组件拆分

// 合理拆分组件,减少虚拟 DOM 比较范围
function App() {
  return (
    <div>
      <Header />
      <MainContent />
      <Footer />
    </div>
  );
}

// 当 MainContent 更新时,Header 和 Footer 不会重新渲染

面试要点

  1. 虚拟 DOM 的本质:JavaScript 对象,描述真实 DOM 的结构
  2. 工作原理:创建虚拟 DOM -> Diff 比较 -> 生成补丁 -> 更新真实 DOM
  3. Diff 策略:同层比较、组件比较、元素比较(key)
  4. 优势:跨平台、性能优化(批量更新)、声明式编程
  5. 局限性:不适合极高频更新、简单操作可能直接操作 DOM 更快

常见追问:

  • 虚拟 DOM 一定比直接操作 DOM 快吗?
  • React 的 Diff 算法时间复杂度是多少?
  • 为什么需要 key?key 的作用是什么?
  • Fiber 架构解决了什么问题?