深入探索 React 源码:最小堆的实现与应用

84 阅读5分钟

在现代编程中,数据结构的合理选择对于程序的性能和可维护性至关重要。React 作为一款广泛使用的前端框架,其内部对数据结构的运用堪称经典。今天,我们就来深入探讨 React 源码中最小堆的实现与应用。

在 Scheduler 中,使用最小堆的数据结构在对任务进行排序。

// 两个任务队列
var taskQueue: Array<Task> = []; 
var timerQueue: Array<Task> = [];
​
push(timerQueue, newTask); // 像数组中推入一个任务
pop(timerQueue); // 从数组中弹出一个任务
timer = peek(timerQueue); // 从数组中获取第一个任务

一、最小堆的基本概念

(一)二叉树

二叉树是一种特殊的树形数据结构,其中每个节点最多有两个子节点。

(二)完全树

完全树则是一种特殊的二叉树,其节点从左到右、从上到下依次填充,最后一层可以不填满,但必须从左到右填充。

例如下面的这些树,就都是完全树:

再例如,下面的这些树,就不是完全树:

完全树中的数值

可以分为两大类:

  • 最大堆:父节点的数值大于或者等于所有的子节点
  • 最小堆:刚好相反,父节点的数值小于或者等于所有的子节点

最大堆示例:

最小堆示例:

  • 无论是最大堆还是最小堆,第一个节点一定是这个堆中最大的或者最小的
  • 每一层并非是按照一定顺序来排列的,比如下面的例子,6可以在左分支,3可以在右分支
  • 每一层的所有元素并非一定比下一层(非自己的子节点)大或者小

二、堆的实现

  • 堆的实现

    堆一般来讲,可以使用数组来实现

    通过数组,我们可以非常方便的找到一个节点的所有亲属

    • 父节点:Math.floor((当前节点的下标 - 1) / 2)
    子节点父节点
    10
    31
    41
    52
    • 左分支节点:当前节点下标 * 2 + 1
    父节点左分支节点
    01
    13
    25
    • 右分支节点:当前节点下标 * 2 + 2
    父节点右分支节点
    02
    14
    26

    react 中对最小堆的应用

    在 react 中,最小堆对应的源码在 SchedulerMinHeap.js 文件中,总共有 6 个方法,其中向外暴露了 3 个方法

    • push:向最小堆推入一个元素
    • pop:弹出一个
    • peek:取出第一个

    没有暴露的是:

    • siftUp:向上调整
    • siftDown:向下调整
    • compare:这是一个辅助方法,就是两个元素做比较的

    所谓向上调整,就是指将一个元素和它的父节点做比较,如果比父节点小,那么就应该和父节点做交换,交换完了之后继续和上一层的父节点做比较,依此类推,直到该元素放置到了正确的位置

    向下调整,就刚好相反,元素往下走,先和左分支进行比较,如果比左分支小,那就交换。

(一)向上调整(siftUp

当一个新节点被插入到最小堆中时,它会被添加到数组的末尾。此时,需要通过向上调整将其移动到合适的位置。向上调整的步骤如下:

  1. 比较新节点与其父节点的值。
  2. 如果新节点的值小于父节点的值,则交换它们的位置。
  3. 重复上述步骤,直到新节点的值大于或等于其父节点的值,或者新节点成为根节点。

(二)向下调整(siftDown

当从最小堆中移除根节点时,需要将最后一个节点移动到根节点的位置,并通过向下调整将其移动到合适的位置。向下调整的步骤如下:

  1. 比较当前节点与其左子节点和右子节点的值。
  2. 如果当前节点的值大于其左子节点或右子节点的值,则将其与较小的子节点交换位置。
  3. 重复上述步骤,直到当前节点的值小于或等于其所有子节点的值,或者当前节点成为叶子节点。

三、React 中最小堆的应用

在 React 的调度器(Scheduler)中,最小堆被用于对任务进行排序。任务按照优先级被存储在最小堆中,优先级越高的任务(数值越小)越先被执行。

(一)peek 方法

peek 方法用于获取最小堆中的最小元素(即根节点),但不移除它。其实现非常简单,直接返回数组的第一个元素即可:

export function peek(heap) {
  return heap.length === 0 ? null : heap[0];
}

(二)push 方法

push 方法用于将一个新任务插入到最小堆中。首先,将新任务添加到数组的末尾,然后通过向上调整将其移动到合适的位置:

export function push(heap, node) {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

(三)pop 方法

pop 方法用于从最小堆中移除最小元素(即根节点)。首先,取出根节点的任务,然后将数组的最后一个任务移动到根节点的位置,并通过向下调整将其移动到合适的位置:

export function pop(heap) {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    heap[0] = last;
    siftDown(heap, last, 0);
  }
  return first;
}

四、总结

通过最小堆的实现与应用,React 的调度器能够高效地对任务进行排序和调度。最小堆的向上调整和向下调整操作确保了任务能够根据优先级被正确地插入和移除。这种数据结构的选择不仅提高了任务调度的效率,也使得代码更加简洁和易于维护。

在日常开发中,我们也可以借鉴 React 的这种设计思路,合理选择数据结构来优化程序的性能。最小堆作为一种高效的数据结构,适用于需要频繁插入和删除最小元素的场景,例如任务调度、优先级队列等。

希望本文对您理解 React 源码中的最小堆实现与应用有所帮助。如果您对最小堆或其他数据结构有进一步的兴趣,欢迎继续探索和学习。