一. Scheduler
介绍
在React
整个构建渲染过程中通过Scheduler
协调任务执行时机,通过优先级机制实现高优任务优先调度执行。其基本思想采用最小堆数据结构,按照任务过期时间进行排序,通过MessageChannel
对象创建宏任务顺序执行任务。
二. 实现Scheduler
任务调度器
2.1 定义Task
对象原型
每当添加一个任务时会创建对应的Task
对象,记录任务优先级,任务回调处理函数等。
function Task() {
this.id = null // 任务唯一id
this.priorityLevel = null // 任务优先级
this.callback = null // 任务回调
this.startTime = null // 任务创建时间
this.expirationTime = null // 任务过期时间
this.sortIndex = null // 任务排序索引
}
2.2 定义任务队列数据结构
Scheduler
管理着一个任务队列,采用最小堆数据结构,按照任务过期时间构建最小堆,即过期时间短的排在前,过期时间长的排在后
// 任务队列
const taskQueue = []
// 添加一个任务
const push = (heap, node) => {
const index = heap.length
heap.push(node)
// 向上调整最小堆
siftUp(heap, node, index)
}
// 删除一个任务
const pop = (heap) => {
if (heap.length === 0) return null
const first = heap[0]
const last = heap.pop()
if (first !== last) {
heap[0] = last
// 向下调整最小堆
siftDown(heap, last, 0)
}
return first
}
// 获取一个任务
const peek = (heap) => {
return heap.length === 0 ? null : heap[0]
}
// 按照过期时间构建最小堆,过期时间短排在前,过期时间长排在后
// 如果过期时间相同的,按照添加任务顺序进行排序,先添加的排在前,后添加的排在后
const compare = (a, b) => {
const diff = a.sortIndex - b.sortIndex
return diff !== 0 ? diff : a.id - b.id
}
// 向上调整最小堆
const siftUp = (heap, node, childIndex) => {
while (childIndex > 0) {
const parentIndex = Math.floor((childIndex - 1) / 2)
const parent = heap[parentIndex]
if (compare(parent, node) > 0) {
heap[parentIndex] = node
heap[childIndex] = parent
childIndex = parentIndex
continue
}
break
}
}
// 向下调整最小堆
const siftDown = (heap, node, parentIndex) => {
while (true) {
let leftChildIndex = parentIndex * 2 + 1
let leftChild = heap[leftChildIndex]
let rightChildIndex = leftChildIndex + 1
let rightChild = heap[rightChildIndex]
if (leftChildIndex >= heap.length) break
if (compare(node, leftChild) > 0) {
if (rightChildIndex < heap.length && compare(leftChild, rightChild) > 0) {
heap[parentIndex] = rightChild
heap[rightChildIndex] = node
parentIndex = rightChildIndex
} else {
heap[parentIndex] = leftChild
heap[leftChildIndex] = node
parentIndex = leftChildIndex
}
} else if (rightChildIndex < heap.length && compare(node, rightChild) > 0) {
heap[parentIndex] = rightChild
heap[rightChildIndex] = node
parentIndex = rightChildIndex
} else {
break
}
}
}
2.3 定义添加任务方法
// 任务计数器
let taskIdCounter = 1
// NormalPriority优先级任务过期时间
const normalPriorityTimeout = 5000 // 5s
/**
* @param {*} priorityLevel 任务优先级
* @param {*} callback 任务回调
* @returns
*/
function scheduleCallback(priorityLevel, callback) {
// 获取当前时间戳
const startTime = performance.now()
// 根据任务优先级获取过期时间
let timeout
switch (priorityLevel) {
// 高优先级任务
case ImmediatePriority:
timeout = -1
break
// 正常优先级任务
case NormalPriority:
timeout = normalPriorityTimeout
break
}
const expirationTime = startTime + timeout
// 创建任务
const task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: expirationTime,
}
push(taskQueue, task)
// 通过MessageChannel对象创建宏任务执行任务队列里的任务
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
channel.port2.postMessage(null)
}
return task
}
2.4 执行任务队列任务
Scheduler
通过MeesgaeChannel
对象创建宏任务执行任务队列里的任务
- 创建
MessageChannel
对象,监听port1
端口接收消息 - 当加添任务时会调用
port2.postMeesage
方法发送消息 - 当
port1
端口接收消息调用performWorkUntilDeadline
方法执行任务队列任务 - 当执行任务时会判断是否已到任务过期时间,如果已到过期时间需要立即执行该任务,如果没有会判断当前
JS
引擎是否处于空闲状态,如果处于空闲状态则执行该任务,否则将该任务作为下一个宏任务执行
const channel = new MessageChannel()
channel.port1.onmessage = performWorkUntilDeadline
function performWorkUntilDeadline() {
if (isMessageLoopRunning) {
let hasMoreWork = false
try {
hasMoreWork = workLoop()
} finally {
if (hasMoreWork) {
channel.port2.postMessage(null)
} else {
isMessageLoopRunning = false
}
}
}
}
const frameYieldMs = 5 // 5纳秒
// 如果时间间隔小于5纳秒,说明JS引擎处于空闲状态
function shouldYieldToHost(startTime) {
return performance.now() - startTime > frameYieldMs
}
function workLoop() {
const currentTime = performance.now()
let currentTask = peek(taskQueue)
while (currentTask !== null) {
// 如果已到任务过期时间则继续执行,如果没有到任务过期时间则判断JS引擎是否处于空闲状态
if (currentTask.expirationTime > currentTime && shouldYieldToHost(currentTime)) {
break
}
// 获取任务回调
const callback = currentTask.callback
currentTask.callback = null
callback(currentTask.expirationTime <= currentTime)
currentTime = performance.now()
// 因为有可能中途插入更高优先级的任务,所以需要判断队列最高优先级的任务和当前执行任务是否是同一个,是才可以删除
if (currentTask === peek(taskQueue)) pop(taskQueue)
currentTask = peek(taskQueue)
}
if (currentTask !== null) return true
return false
}