React 源码解析之最小堆

201 阅读6分钟

现在很多面试都会考查react的调度原理,底层是如何实现的?相信学完本堂课,你一定会有更深的体会,斩获更多的offer

React调度器之最小堆

大家在使用react的时候,是不是觉得写代码非常的流畅,界面交互也非常的丝滑?

这一切的根因都是因为react在底层已经帮我们做好了一系列任务的调度,称之为scheduler

主流浏览器的刷新频率为16.6ms/次,我们知道js进程和ui进程是互斥的,当js执行时间过长时,浏览器将没有时间来做ui的刷新,就会造成卡顿的现象。

react为了解决这个问题,使用了时间切片的算法,把长任务打散成一个个短任务,这其实和我们的操作系统调度是同样的原理。这样我们就可以在执行完短任务之后,来判断剩余时间是否足够浏览器进行渲染

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    ...
}
switch (priorityLevel) {
    case ImmediatePriority:
      // Times out immediately
      timeout = -1;
      break;
    case UserBlockingPriority:
      // Eventually times out
      timeout = userBlockingPriorityTimeout;
      break;
    case IdlePriority:
      // Never times out
      timeout = maxSigned31BitInt;
      break;
    case LowPriority:
      // Eventually times out
      timeout = lowPriorityTimeout;
      break;
    case NormalPriority:
    default:
      // Eventually times out
      timeout = normalPriorityTimeout;
      break;
  }

  var expirationTime = startTime + timeout;

这是react工作循环部分源码,通过高亮部分我们可以看到,每个任务都有过期时间,当任务过期之后将会中断当前的工作循环

而expirationTime是由任务的开始时间和不同任务的优先级的timout决定,也就是说我们会给高优先级的任务较短的过期时间。那么问题来了,我们在一次更新当中会有非常多的任务,如何使用一种算法能够快速的获取到过期时间最短的任务呢?答案就是最小堆 O(1)

最小堆原理

定义

最小堆,是一种经过排序的完全二叉树,其中任一非终端节点的数据值均不大于其左子结点和右子结点的值

完全二叉树

一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

满二叉树

一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的深度为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

代码实现

我们最终使用数组来模拟二叉树,我们可以把序号0看作根节点可以推导出小根堆有两个性质

  • 根节点为i 左子节点为2i + 1, 右子节点为2i+2
  • 根节点为当前这颗树中最小的值

思路

我们需要在不破坏小根堆性质的同时,不断的往树中添加节点。同时,我们可以随时取出堆中最小的节点,并且经过调整让树重新符合小根堆的性质。

伪代码

// 当前所有的节点
const array = [2, 7, 5, 12, 22, 17, 25]

const BuildMinHeap = (array: number[], len: number) => {
  遍历所有节点
  依次推入堆中
  做一次向上调整
}

const push = (heap: number[], node: number) => {
  将节点推入队尾
  向上调整保证性质
}

const pop = (heap: number[]) => {
  从队首把节点弹出
  把队尾的节点换到队首
  向下调整保证性质
}

const peek = (heap: number[]) => {
  返回队首节点
}

const siftUp = (heap: number[], node: number, i: number) => {
  找到父节点
  循环
    和父节点比较谁更小
      父节点小则保持不动
      子节点小则交换位置
    
}

const siftDown = () => {
  找到两个字节点
  循环
    依次判断子节点和当前节点的大小
      当前节点小则保持不动
      子节点小则和子节点交换位置
}
const array = [2, 7, 5, 12, 22, 17, 25]

const push = (heap: number[], node: number) => {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index)
}

const pop = (heap: number[], node: number) => {
  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;
}


const peek =(heap: number[])=> {
  return heap.length === 0 ? null : heap[0];
}


const siftUp = (heap: number[], node: number, i: number) => {
  let index = i;
  // edge case
  while(index > 0) {
    const parentIndex = (i-1)/2
    const parent = heap[parentIndex]
    if(parent > node) {
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      return
    }
  }
}

const siftDown = () => {
  let index = i;
  const length = heap.length;
  const halfLength = length / 2;
  while (index < halfLength) {
    const leftIndex = 2 * index + 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    if (left < node) {
      if (rightIndex < length && right < left) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && right) < node) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      return;
    }
  }
}

react源码如何实现最小堆

github.com/facebook/re…

思路是一致的

源码中的node是个复杂的结构,所以需要compare来抹平比较的差异,同时使用了位运算来更加快速的做整除操作

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict
 */

type Heap<T: Node> = Array<T>;
type Node = {
  id: number,
  sortIndex: number,
  ...
};

export function push<T: Node>(heap: Heap<T>, node: T): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

export function peek<T: Node>(heap: Heap<T>): T | null {
  return heap.length === 0 ? null : heap[0];
}

export function pop<T: Node>(heap: Heap<T>): T | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    // $FlowFixMe[incompatible-type]
    heap[0] = last;
    // $FlowFixMe[incompatible-call]
    siftDown(heap, last, 0);
  }
  return first;
}

function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
  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<T: Node>(heap: Heap<T>, node: T, i: number): void {
  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: Node, b: Node) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

总结

其实在许多的面试环节当中,我们都会被问到react的调度原理,学完这节课后,相信大家一定都能够回答上来。同学们也要多多动手实现一下,提升自己的coding能力,在面试大厂的时候,碰到面试官给的算法题,能够啪一下,秒了(当然,也不能秒的太快,要体现自己思考的过程😄)

课后习题

通过本堂课的学习,我们已经很好的了解了如何实现一个最小堆。那西蒙老师给大家留一个课后作业。

给定一个数组,我们想要找到数组中第k小的元素,应该如何用最小堆去实现呢?大家可以尝试一下