持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情
在上一节中,我们利用的是浏览器的 requestIdleCallback 实现 scheduler,这一节我们自己实现一个 scheduler。
👉 仓库地址,跪求您帮忙点个 🌟🌟,谢谢啦~
PS:本节代码在 v0.0.2 分支
React 中的任务池,不同的任务有不同的优先级,React 需要优先处理优先级高的任务,比如用户的输入响应可能就要比页面加载优先级更高一些。 存储任务的任务池有两个:
taskQueue存储的是立即要执行的任务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 分支