02|实现 React scheduler (调度模块)的核心代码

215 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

在上一节中,我们利用的是浏览器的 requestIdleCallback 实现 scheduler,这一节我们自己实现一个 scheduler。

👉 仓库地址,跪求您帮忙点个 🌟🌟,谢谢啦~

PS:本节代码在 v0.0.2 分支

React 中的任务池,不同的任务有不同的优先级,React 需要优先处理优先级高的任务,比如用户的输入响应可能就要比页面加载优先级更高一些。 存储任务的任务池有两个:

  1. taskQueue存储的是立即要执行的任务
  2. timerQueue存储的是可以延迟执行的任务
const taskQueue = []
const timerQueue = []

在 React 中,任务的初始结构定义如下:

const newTask = {
    id: taskIdCounter++, // 标记任务 id
    callback, // 回调函数
    priorityLevel,	// 任务优先级
    startTime,	// 任务开始时间,时间点
    expirationTime,	// 过期时间,时间点
    sortIndex: -1, // 任务排序,取值来自过期时间,因此值越小,优先级越高
}

React中一旦来了新任务,就会先用currentTime记录当前时间(performance.now()或者Date.now()),如果任务有delay参数,那么任务开始执行时间startTime = currentTime + delay;。接下来通过startTime > currentTime如果成立,证明任务是可以延期的,那么任务进入timerQueue,否则进入taskQueue。

实现优先队列

这里我们因为要维护一个最值,所以选用最小堆。 src/scheduler/SchedulerMinHeap.ts

type HeapNode = {
    id: number,
    sortIndex: number
}
type Heap = Array<HeapNode>

// 返回堆顶元素
function peek(heap: Heap) {
    return heap.length ? heap[0] : null
}

// 往最小堆中插入元素
function push(heap: Heap, node: HeapNode) {
    let index = heap.length
    heap.push(node)
    shiftUp(heap, index)
}

function shiftUp(heap: Heap, index: number) {
    while (index) {
        const parentIndex = (index - 1) >> 1
        if (compare(heap[parentIndex], heap[index]) > 0) {
            // 父节点大,位置交换
            swap(heap, parentIndex, index)
            index = parentIndex
        } else {
            return
        }
    }
}

// 删除堆顶元素
function pop(heap: Heap) {
    if (heap.length === 0) {
        return null
    }
    const first = heap[0]
    const last = heap.pop()

    if (first !== last) {
        heap[0] = last
        shiftDown(heap, 0)
    }

    return first
}

function shiftDown(heap: Heap, index: number) {
    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 (compare(left, heap[index]) < 0) { // 发现左边节点比当前节点小
            if (rightIndex < length && compare(right, left) < 0) { // 但是左边节点还得再跟右边节点比一下,如果右边更小,则当前节点与右边交换
                swap(heap, rightIndex, index)
                index = rightIndex
            } else {
                swap(heap, leftIndex, index)
                index = leftIndex
            }

        } else if (rightIndex < length && compare(right, heap[index]) < 0) { // 右边节点小一些
            swap(heap, rightIndex, index)
            index = rightIndex
        } else {
            return
        }
    }
}

function swap(heap: Heap, j: number, k: number) {
    [heap[j], heap[k]] = [heap[k], heap[j]]
}

function compare(a: HeapNode, b: HeapNode) {
    // return a - b
    const diff = a.sortIndex - b.sortIndex
    return diff !== 0 ? diff : a.id - b.id
}

实现任务调度

这里我们利用MessageChannel 实现 scheduler src/scheduler/index.ts

import { peek, pop, push } from "./SchedulerMinHeap"
interface Task {
    id: number,
    callback: any,
    expirtationTime: number,
    // sortIndex 暂时使用过期时间
    sortIndex: number
}

const taskQueue: Task[] = []
let taskIdCounter = 1

export function scheduleCallback(callback) {
    const currentTime = getCurrentTime()

    // 暂时还没有优先级,暂时都不做等待
    const timeout = -1

    // 过期时间
    const expirtationTime = currentTime - timeout

    const newTask = {
        id: taskIdCounter++,
        callback,
        expirtationTime,
        // sortIndex 暂时使用过期时间
        sortIndex: expirtationTime 
    }

    push(taskQueue, newTask)

    // 请求调度
    requestHostCallback()
}

function requestHostCallback() {
    port.postMessage(null)
}

// 创建宏任务
const channel = new MessageChannel()
const port = channel.port2

channel.port1.onmessage = function () {
    // 通过接收消息来执行任务
    workLoop()
}

function workLoop() {
    let currentTask = peek(taskQueue) as Task
    while(currentTask) {
        const callback = currentTask.callback
        currentTask.callback = null
        callback()
        pop(taskQueue)
        // 继续取任务执行
        currentTask = peek(taskQueue) as Task
    }
}

export function getCurrentTime() {
	return performance.now()
}

因为我们还没有实现优先级,所以这里的 scheduleCallback 还不具备任务调度的能力(后面会加)。 我们这里先替换掉requestIdleCallback为我们自己实现的scheduleCallback,让页面能跑起来。 src/ReactFiberWorkLoop.ts

export function scheduleUpdateOnFiber(fiber: Fiber) {
    workInProgress = fiber;
    workInProgressRoot = fiber

    // 在这里进行任务调度
    scheduleCallback(workLoop)
}

// 修改一下 workLoop 函数
function workLoop() {
    while(workInProgress) {
        performUnitOfWork()
    }

    if (!workInProgress && workInProgressRoot) {
        commitRoot()
    }
}

// requestIdleCallback(workLoop)

尽管我们的任务还没有优先级,但到这里页面也能做到展示了。当然后续会随着功能的增加,将欠缺的功能补上。这一节最主要的就是实现一个最小堆。这个数据结构就是我们获取任务优先级的关键,希望你能掌握它,对应在 leetcode 的 703. 数据流中的第 K 大元素 可以作为你的练习。

仓库地址

最后附上 👉 仓库地址,跪求您帮忙点个 🌟🌟,谢谢啦~

PS:本节代码在 v0.0.2 分支

image.png