从0到1实现react(三):任务调度-最小堆基础

247 阅读3分钟

为什么要学最小堆?

想象一下,你是一个超级忙碌的项目经理,手上有无数个任务等待处理。有些任务紧急程度高,有些相对不那么着急。你会怎么安排处理顺序呢?

这就是 React 在进行任务调度时面临的问题!每当页面需要更新时,React 需要决定哪些任务优先级最高,应该先处理。而最小堆(Min Heap)就是 React Scheduler 用来解决这个问题的核心数据结构。

今天,我们就来揭开最小堆的神秘面纱,看看它是如何帮助 React 高效管理任务优先级的!

什么是最小堆?一个生动的比喻 🏆

想象最小堆就像一个"自动排序的金字塔":

  • 塔顶永远是最小的数(就像最高优先级的任务总在最前面)
  • 每个父节点都比子节点小(上司的优先级总是比下属高)
  • 插入新元素时,金字塔会自动重新排列(新任务来了会自动找到合适位置)

这种特性让我们可以在 O(log n) 的时间复杂度内插入任务,并在 O(1) 的时间内获取最高优先级的任务!

最小堆的结构:从抽象到具体

1. 树形结构视图

让我们先看看最小堆长什么样:

graph TD
    A[1] --> B[3]
    A --> C[4]
    B --> D[7]
    B --> E[10]
    C --> F[9]
    C --> G[6]
    D --> H[15]
    D --> I[14]
    E --> J[12]

    style A fill:#ff9999
    style B fill:#ffcc99
    style C fill:#ffcc99
    style D fill:#ffffcc
    style E fill:#ffffcc
    style F fill:#ffffcc
    style G fill:#ffffcc
    style H fill:#ccffcc
    style I fill:#ccffcc

看出规律了吗?

  • 根节点 1 是最小的
  • 每个父节点都比它的子节点小
  • 这就保证了堆顶永远是最小值!

2. 数组存储:化繁为简

虽然最小堆在逻辑上是树结构,但在实际存储时,我们使用数组!这是个巧妙的设计:

graph LR
    subgraph "数组索引"
        idx0[0]
        idx1[1]
        idx2[2]
        idx3[3]
        idx4[4]
        idx5[5]
        idx6[6]
        idx7[7]
        idx8[8]
        idx9[9]
    end

    subgraph "数组值"
        val1[1]
        val3[3]
        val4[4]
        val7[7]
        val10[10]
        val9[9]
        val6[6]
        val15[15]
        val14[14]
        val12[12]
    end

    idx0 --- val1
    idx1 --- val3
    idx2 --- val4
    idx3 --- val7
    idx4 --- val10
    idx5 --- val9
    idx6 --- val6
    idx7 --- val15
    idx8 --- val14
    idx9 --- val12

数组 = [1, 3, 4, 7, 10, 9, 6, 15, 14, 12]

3. 索引关系:数学的魅力

这里有个超酷的数学规律:

graph TD
subgraph "索引关系"
A["父节点索引 i"]
B["左子节点: 2*i + 1"]
C["右子节点: 2*i + 2"]
D["子节点 j 的父节点: (j-1)÷2"]

        A --> B
        A --> C
        D --> A
    end

举个例子:

  • 索引 0 的左子节点:2×0+1 = 1
  • 索引 0 的右子节点:2×0+2 = 2
  • 索引 3 的父节点:(3-1)÷2 = 1

这个规律让我们可以仅用数组就完美模拟树结构!

代码实现:从理论到实践

现在让我们动手实现一个最小堆类:

/**
 * 最小堆的实现 - React Scheduler 的核心数据结构
 * 让任务按优先级自动排序!
 */
class MiniHeap {
  heap = [];

  constructor() {
    console.log("🎯 最小堆初始化完成!");
  }

  /**
   * 插入新任务 - 就像给金字塔添加新砖块
   * @param {number} val 任务的优先级值(越小优先级越高)
   */
  insert(val) {
    console.log(`📥 插入新任务,优先级: ${val}`);
    this.heap.push(val);
    
    // 如果不是第一个元素,需要向上调整位置
    if (this.heap.length > 1) {
      this.heapifyUp();
    }
    console.log(`当前堆状态: [${this.heap.join(', ')}]`);
  }

  /**
   * 获取并移除最高优先级任务 - 取走金字塔顶端
   * @returns {number|undefined} 最高优先级的任务
   */
  pop() {
    if (this.heap.length === 0) {
      console.log("⚠️ 没有任务可以执行了!");
      return undefined;
    }
    
    if (this.heap.length === 1) {
      const task = this.heap.pop();
      console.log(`🎯 执行最后一个任务: ${task}`);
      return task;
    }

    // 保存最小值(堆顶)
    let min = this.heap[0];
    console.log(`🎯 执行最高优先级任务: ${min}`);
    
    // 将最后一个元素移到顶部,然后向下调整
    this.heap[0] = this.heap.pop();
    this.heapifyDown();
    
    console.log(`执行后堆状态: [${this.heap.join(', ')}]`);
    return min;
  }

  /**
   * 获取父节点索引 - 找到上级
   */
  getParentIndex(i) {
    return Math.floor((i - 1) / 2);
  }

  /**
   * 获取左子节点索引 - 找到左下属
   */
  getLeft(i) {
    return 2 * i + 1;
  }

  /**
   * 获取右子节点索引 - 找到右下属
   */
  getRight(i) {
    return 2 * i + 2;
  }

  /**
   * 向上调整 - 新员工可能比老板还优秀,需要晋升!
   */
  heapifyUp() {
    let currentIndex = this.heap.length - 1;
    
    while (currentIndex > 0) {
      let parentIndex = this.getParentIndex(currentIndex);
      
      // 如果当前节点比父节点小,就交换位置(向上晋升)
      if (this.heap[currentIndex] < this.heap[parentIndex]) {
        console.log(`🔄 交换位置: ${this.heap[currentIndex]}${this.heap[parentIndex]}`);
        [this.heap[currentIndex], this.heap[parentIndex]] = [
          this.heap[parentIndex],
          this.heap[currentIndex],
        ];
        currentIndex = parentIndex;
      } else {
        break; // 已经找到合适位置
      }
    }
  }

  /**
   * 向下调整 - 新来的老板可能不如下属,需要降级!
   */
  heapifyDown() {
    let currentIndex = 0;

    while (true) {
      let leftIndex = this.getLeft(currentIndex);
      let rightIndex = this.getRight(currentIndex);
      let smallestIndex = currentIndex;

      // 找到当前节点和其子节点中最小的
      if (
        leftIndex < this.heap.length &&
        this.heap[leftIndex] < this.heap[smallestIndex]
      ) {
        smallestIndex = leftIndex;
      }

      if (
        rightIndex < this.heap.length &&
        this.heap[rightIndex] < this.heap[smallestIndex]
      ) {
        smallestIndex = rightIndex;
      }

      // 如果当前节点已经是最小的,说明位置合适
      if (smallestIndex === currentIndex) {
        break;
      }
      
      console.log(`🔄 向下调整: ${this.heap[currentIndex]}${this.heap[smallestIndex]} 交换`);
      [this.heap[currentIndex], this.heap[smallestIndex]] = [
        this.heap[smallestIndex],
        this.heap[currentIndex],
      ];
      currentIndex = smallestIndex;
    }
  }

  /**
   * 获取堆大小
   */
  size() {
    return this.heap.length;
  }

  /**
   * 查看最高优先级任务(不移除)
   */
  peek() {
    return this.heap.length > 0 ? this.heap[0] : undefined;
  }
}

// 🎮 让我们来测试一下!
console.log("=== 最小堆演示开始 ===");

const heap = new MiniHeap();

console.log("\n📋 模拟 React 任务调度场景:");
console.log("优先级越小越重要(1=超高优先级,100=低优先级)");

// 插入一些"任务"
heap.insert(100); // 低优先级任务
heap.insert(10);  // 中等优先级
heap.insert(9);   // 较高优先级  
heap.insert(7);   // 高优先级任务

console.log("\n🎯 开始执行任务(按优先级从高到低):");
while (heap.size() > 0) {
  const nextTask = heap.pop();
  console.log(`执行任务,优先级: ${nextTask}`);
}

console.log("\n=== 演示结束 ===");

实战应用:React 中的任务调度

在 React 的 Scheduler 中,最小堆被用来管理任务队列:

  1. 插入任务:当组件需要更新时,React 会根据优先级插入任务
  2. 执行任务:Scheduler 总是取出优先级最高的任务执行
  3. 时间片管理:如果时间片用完,当前任务会被重新插入堆中

这种设计让 React 可以:

  • 响应用户交互(高优先级)
  • 🎨 处理动画更新(中等优先级)
  • 📊 执行后台数据同步(低优先级)

性能分析:为什么选择最小堆?

操作时间复杂度说明
插入O(log n)最多需要向上调整 log n 层
删除最小值O(log n)最多需要向下调整 log n 层
查看最小值O(1)直接访问数组第一个元素
构建堆O(n)批量构建时的优化算法

相比普通数组:

  • ❌ 插入后排序:O(n log n)
  • ❌ 查找最小值:O(n)
  • ✅ 最小堆插入:O(log n)
  • ✅ 最小堆取最小值:O(1)