手写React Scheduler,理解Scheduler原理

160 阅读3分钟

一. 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
}

三. 往期文章推荐

3.1 React原理系列总结

四. 参考文档

4.1 performance.now()
4.2 queueMicrotask
4.3 MessageChannel