37. React 的 Diffing 算法大致原理?(同层比较、key 优化、组件类型比对)

40 阅读5分钟

React 面试题详细答案 - 第 37 题

37. React 的 Diffing 算法大致原理?(同层比较、key 优化、组件类型比对)

Diffing 算法概述

React 的 Diffing 算法是虚拟 DOM 的核心,用于比较新旧虚拟 DOM 树,找出需要更新的最小变化集,然后只更新真实 DOM 中发生变化的部分。

为什么需要 Diffing 算法?
// 没有 Diffing 算法的问题
// 每次状态更新都重新渲染整个 DOM 树
function updateDOM() {
  // 清空整个 DOM
  document.body.innerHTML = ''
  // 重新创建所有元素
  renderAllComponents()
}

// 有 Diffing 算法的优势
// 只更新发生变化的部分
function updateDOM() {
  const changes = diff(oldVDOM, newVDOM)
  applyChanges(changes) // 只更新变化的部分
}

三大核心策略

1. 同层比较策略

React 只比较同一层级的节点,不会跨层级比较。

// 示例:同层比较
// 旧树
<div>
  <ComponentA />
  <ComponentB />
</div>

// 新树
<div>
  <ComponentA />
  <ComponentC />  // 只比较这一层
</div>

// React 会:
// 1. 比较 div 标签(相同,不更新)
// 2. 比较第一个子节点 ComponentA(相同,不更新)
// 3. 比较第二个子节点 ComponentB vs ComponentC(不同,替换)
2. 组件类型比对

不同类型的组件会被完全替换。

// 组件类型不同,完全重新渲染
function OldComponent() {
  return <div>Old</div>
}

function NewComponent() {
  return <span>New</span>
}

// React 会:
// 1. 卸载 OldComponent
// 2. 挂载 NewComponent
// 3. 不会尝试复用任何内容
3. Key 优化策略

使用 key 来标识列表中的元素,帮助 React 识别哪些元素发生了变化。

// 没有 key 的问题
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li>{todo.text}</li> // 没有 key
      ))}
    </ul>
  )
}

// 有 key 的优化
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li> // 使用 key
      ))}
    </ul>
  )
}

Diffing 算法详细流程

1. 元素类型比较
// 伪代码:元素类型比较
function diffElement(oldElement, newElement) {
  // 类型不同,完全替换
  if (oldElement.type !== newElement.type) {
    return {
      type: 'REPLACE',
      oldElement,
      newElement,
    }
  }

  // 类型相同,比较属性和子元素
  return {
    type: 'UPDATE',
    props: diffProps(oldElement.props, newElement.props),
    children: diffChildren(oldElement.children, newElement.children),
  }
}
2. 属性比较
// 属性比较逻辑
function diffProps(oldProps, newProps) {
  const changes = []

  // 检查新增和修改的属性
  for (const key in newProps) {
    if (oldProps[key] !== newProps[key]) {
      changes.push({
        type: 'SET_PROP',
        key,
        value: newProps[key],
      })
    }
  }

  // 检查删除的属性
  for (const key in oldProps) {
    if (!(key in newProps)) {
      changes.push({
        type: 'REMOVE_PROP',
        key,
      })
    }
  }

  return changes
}
3. 子元素比较
// 子元素比较(简化版)
function diffChildren(oldChildren, newChildren) {
  const changes = []
  const maxLength = Math.max(oldChildren.length, newChildren.length)

  for (let i = 0; i < maxLength; i++) {
    const oldChild = oldChildren[i]
    const newChild = newChildren[i]

    if (!oldChild) {
      // 新增子元素
      changes.push({
        type: 'INSERT',
        index: i,
        element: newChild,
      })
    } else if (!newChild) {
      // 删除子元素
      changes.push({
        type: 'REMOVE',
        index: i,
      })
    } else {
      // 递归比较子元素
      const childChanges = diffElement(oldChild, newChild)
      if (childChanges) {
        changes.push({
          type: 'UPDATE',
          index: i,
          changes: childChanges,
        })
      }
    }
  }

  return changes
}

Key 的作用机制

1. 没有 Key 的问题
// 问题示例:列表重排序
const oldList = [<div>Item 1</div>, <div>Item 2</div>, <div>Item 3</div>]

const newList = [
  <div>Item 3</div>, // 移动到第一位
  <div>Item 1</div>,
  <div>Item 2</div>,
]

// 没有 key 时,React 会:
// 1. 比较第一个元素:Item 1 vs Item 3(不同,替换)
// 2. 比较第二个元素:Item 2 vs Item 1(不同,替换)
// 3. 比较第三个元素:Item 3 vs Item 2(不同,替换)
// 结果:3 个 DOM 元素都被替换,性能差
2. 有 Key 的优化
// 优化示例:使用 key
const oldList = [
  <div key="1">Item 1</div>,
  <div key="2">Item 2</div>,
  <div key="3">Item 3</div>,
]

const newList = [
  <div key="3">Item 3</div>, // 移动到第一位
  <div key="1">Item 1</div>,
  <div key="2">Item 2</div>,
]

// 有 key 时,React 会:
// 1. 找到 key="3" 的元素,移动到第一位
// 2. 找到 key="1" 的元素,移动到第二位
// 3. 找到 key="2" 的元素,移动到第三位
// 结果:只是 DOM 元素移动,没有重新创建,性能好
3. Key 的最佳实践
// 好的 key 选择
function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li> // 使用唯一 ID
      ))}
    </ul>
  )
}

// 避免使用数组索引作为 key(在列表会变化时)
function BadExample({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.name}</li> // 不推荐
      ))}
    </ul>
  )
}

// 当没有唯一 ID 时,可以组合多个属性
function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={`${product.category}-${product.name}`}>{product.name}</li>
      ))}
    </ul>
  )
}

组件级别的 Diffing

1. 函数组件比较
// 函数组件:比较函数引用
function MyComponent({ name }) {
  return <div>Hello {name}</div>
}

// 如果函数组件没有变化,React 会跳过重新渲染
// 如果 props 没有变化,React 会跳过重新渲染
2. 类组件比较
// 类组件:比较类引用
class MyComponent extends React.Component {
  render() {
    return <div>Hello {this.props.name}</div>
  }
}

// React 会:
// 1. 比较类组件类型
// 2. 比较 props
// 3. 如果都相同,跳过重新渲染
3. 组件更新优化
// 使用 React.memo 优化函数组件
const MyComponent = React.memo(function MyComponent({ name, age }) {
  return (
    <div>
      {name} is {age} years old
    </div>
  )
})

// 使用 PureComponent 优化类组件
class MyComponent extends React.PureComponent {
  render() {
    return <div>Hello {this.props.name}</div>
  }
}

// 自定义比较函数
const MyComponent = React.memo(
  function MyComponent({ user }) {
    return <div>{user.name}</div>
  },
  (prevProps, nextProps) => {
    // 返回 true 表示 props 相同,跳过重新渲染
    return prevProps.user.id === nextProps.user.id
  }
)

实际应用示例

1. 列表渲染优化
// 优化前:性能问题
function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem
          key={index} // 使用索引作为 key
          todo={todo}
          onToggle={onToggle}
        />
      ))}
    </ul>
  )
}

// 优化后:使用唯一 key
function TodoList({ todos, onToggle }) {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id} // 使用唯一 ID
          todo={todo}
          onToggle={onToggle}
        />
      ))}
    </ul>
  )
}

// 进一步优化:使用 React.memo
const TodoItem = React.memo(function TodoItem({ todo, onToggle }) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      {todo.text}
    </li>
  )
})
2. 条件渲染优化
// 条件渲染:使用 key 强制重新渲染
function UserProfile({ user, isEditing }) {
  if (isEditing) {
    return <EditForm key={user.id} user={user} />
  }
  return <DisplayProfile key={user.id} user={user} />
}

// 或者使用不同的组件类型
function UserProfile({ user, isEditing }) {
  return isEditing ? <EditForm user={user} /> : <DisplayProfile user={user} />
}
3. 动态组件优化
// 动态组件:使用 key 确保正确更新
function DynamicComponent({ type, props }) {
  const components = {
    button: Button,
    input: Input,
    select: Select,
  }

  const Component = components[type]
  return <Component key={type} {...props} />
}

// 避免在渲染中创建新对象
function BadExample({ items }) {
  return (
    <div>
      {items.map((item) => (
        <Item
          key={item.id}
          item={item}
          onClick={() => handleClick(item)} // 每次渲染都创建新函数
        />
      ))}
    </div>
  )
}

// 优化:使用 useCallback
function GoodExample({ items }) {
  const handleClick = useCallback((item) => {
    // 处理点击
  }, [])

  return (
    <div>
      {items.map((item) => (
        <Item key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  )
}

性能优化技巧

1. 避免不必要的重新渲染
// 使用 useMemo 缓存计算结果
function ExpensiveComponent({ data, filter }) {
  const filteredData = useMemo(() => {
    return data.filter((item) => item.category === filter)
  }, [data, filter])

  return (
    <div>
      {filteredData.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </div>
  )
}

// 使用 useCallback 缓存函数
function ParentComponent() {
  const [count, setCount] = useState(0)

  const handleClick = useCallback(() => {
    setCount((prev) => prev + 1)
  }, [])

  return <ChildComponent onClick={handleClick} />
}
2. 合理使用 key
// 在列表变化时使用稳定的 key
function SortableList({ items, sortBy }) {
  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => a[sortBy] - b[sortBy])
  }, [items, sortBy])

  return (
    <ul>
      {sortedItems.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  )
}

// 在组件状态重置时使用 key
function Form({ initialData }) {
  const [data, setData] = useState(initialData)

  return (
    <form key={initialData.id}>
      {' '}
      {/* 当 initialData.id 变化时,表单会重置 */}
      <input
        value={data.name}
        onChange={(e) => setData({ ...data, name: e.target.value })}
      />
    </form>
  )
}

总结

React 的 Diffing 算法通过以下策略实现高效更新:

  1. 同层比较:只比较同一层级的节点,避免跨层级比较的复杂性
  2. 组件类型比对:不同类型的组件直接替换,相同类型的组件进行属性比较
  3. Key 优化:使用 key 标识列表元素,实现最小化 DOM 操作

关键优化点:

  • 使用稳定的、唯一的 key
  • 避免在渲染中创建新对象和函数
  • 合理使用 React.memo、useMemo、useCallback
  • 理解组件更新时机,避免不必要的重新渲染

掌握这些原理和技巧,可以显著提升 React 应用的性能。