「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」
react 版本:v17.0.3
二叉堆
二叉堆是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆有两种:最大堆和最小堆。
最大堆:父结点的键值总是大于或等于任何一个子节点的键值;
最小堆:父结点的键值总是小于或等于任何一个子节点的键值。
堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
- 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
- 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
我们对上图中的每个数进行标记,上面的结构映射成数组就变成了下面的样子
React 中的使用场景
在 React 中,并发任务经过 Scheduler 调度时,会被 Scheduler 包装成自己的task,这个task会被保存到最小堆timerQueue 或 taskQueue 中,以便可以在O(1) 的时间复杂度获取到优先级最高的 task。
// packages/scheduler/src/forks/Scheduler.js
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
// timerQueue 是一个二叉堆结构,以最小堆的形式存储 task
// 向二叉堆中添加节点
push(timerQueue, newTask);
// peek 查看堆的顶点, 也就是优先级最高的`task`
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// ...
}
} else {
newTask.sortIndex = expirationTime;
// taskQueue 是一个二叉堆结构,以最小堆的形式存储 task
push(taskQueue, newTask);
// ...
}
最小堆的源码实现在SchedulerMinHeap.js文件:
// src/react/packages/scheduler/src/SchedulerMinHeap.js
// 二叉堆数据结构
// 主要存储taskQueue 和 timerQueue,它们都是以 最小堆的形式存储
// 可以保证以O(1)的时间复杂度, 取到数组顶端的对象(优先级最高的 task).
type Heap = Array<Node>;
type Node = {|
id: number,
sortIndex: number,
|};
// 插入节点,一般是在数组的末尾插入新节点
// 然后调用 siftUp函数向上调整堆
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
// 查看 堆的顶点,也就是优先级最高的 task
export function peek(heap: Heap): Node | null {
return heap.length === 0 ? null : heap[0];
}
// 删除节点,即删除堆顶节点,也就是取出最高优先级的 task
// 删除顶点之后,需要调用 siftDown 函数向下调整堆
export function pop(heap: Heap): Node | null {
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;
}
// 当插入节点之后, 需要向上调整堆结构, 保证数组是一个最小堆.
function siftUp(heap, node, i) {
let index = i;
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
// 向下调整堆结构, 保证数组是一个最小堆.
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (compare(left, node) < 0) {
if (rightIndex < length && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
-
push 函数:插入新节点,将新节点插入到数组的末尾,然后调用 siftUp函数向上调整堆
-
peek 函数:查看 堆的顶点,也就是优先级最高的 task
-
pop 函数:删除堆顶节点,也就是取出最高优先级的 task,然后需要调用 siftDown 函数向下调整堆
-
siftUp 函数:当插入新节点之后, 需要向上调整堆结构, 保证数组是一个最小堆
-
siftDown 函数:向下调整堆结构, 保证数组是一个最小堆
-
compare 函数:比较两个节点值的大小
总结
React 在实现任务调度时,使用最小堆存储 Scheduler 的 task,使得在任务调度的过程中可以在O(1)的时间获取到优先级最高的task,提高了任务调度的效率。这种设计思想值得我们借鉴学习。