Vue3.0 - 源码解读 - 调度系统

一、前言

vue3.0是如何对框架执行中的任务进行调度的呢?我们知道js是单线程的,如果所有任务都是一鼓作气的同步去执行,会导致主线程的阻塞,那么当你想在表单输入东西时,由于主线程被其他任务占据,所以你的输入任务是无法立即得到响应的。调度系统是为了解决这个问题。
vue的调度系统相对比较轻,并没有像react那样引入fiber这么重的调度算法(做了快两年),而是巧妙的运用了js微任务机制,将更新触发的任务放到一个微任务中,这样就保证在有大量更新任务时,主线程不会被阻塞,等主线程任务执行完毕后,微任务中的更新开始执行。

二、源码分析

先看下调度器声明的一些变量,相对于调度器本身来说,这些是全局变量,用于调度过程中的一些状态记录。

// 任务队列是否正在排空
let isFlushing = false
// 微任务已创建,任务队列等待排空
let isFlushPending = false

// 主任务队列,用于存储更新任务
const queue: (SchedulerJob | null)[] = []
// 当前正在执行的任务在主任务队列中的索引
let flushIndex = 0

// 框架运行过程中产生的前置回调任务,比如一些特定的生命周期
// 这些回调任务是在主任务队列queue开始排空前批量排空执行的
const pendingPreFlushCbs: SchedulerCb[] = []
// 当前激活的前置回调任务
let activePreFlushCbs: SchedulerCb[] | null = null
// 当前前置回调任务在队列中的索引
let preFlushIndex = 0

// 框架运行过程中产生的后置回调任务,比如一些特定的生命周期(onMounted等)
// 这些回调任务是在主任务队列queue排空后批量排空执行的
const pendingPostFlushCbs: SchedulerCb[] = []
// 当前激活的后置回调任务
let activePostFlushCbs: SchedulerCb[] | null = null
// 当前后置回调任务在队列中的索引
let postFlushIndex = 0

// 微任务创建器
const resolvedPromise: Promise<any> = Promise.resolve()
// 当前微任务promise
let currentFlushPromise: Promise<void> | null = null

let currentPreFlushParentJob: SchedulerJob | null = null

// 同一个任务递归执行的上限次数
const RECURSION_LIMIT = 100
// 记录每个任务执行的次数
type CountMap = Map<SchedulerJob | SchedulerCb, number>

queueJob
调度系统的核心处理逻辑,将更新任务推入主任务队列,同时会在合适的时机创建微任务,在微任务中执行任务并排空任务队列,做批量的更新工作。
vue model层更新,并不是立即出发view更新的,原因上面我们也提到了,大量的同步更新,比如一个由父到子的递归更新,函数调用栈会长时间占用主线程,导致线程阻塞无法执行其他更重要的任务。因此vue将更新时产生的任务缓存到任务队列,在微任务中批量执行。

export function queueJob(job: SchedulerJob) {
  // 主任务可入队逻辑:1. 队列为空 2. 正在清空队列(有正在执行的任务)且当前待入队任务
  // 是允许递归执行本身的,由于任务可能递归执行自身,该情况下待入队任务一定和当前执行任务
  // 是同一任务,因此待入队任务和正在执行任务相同,但不能和后面待执行任务相同 3. 其他情况下,
  // 由于不会出现任务自身递归执行的情况,因此待入队任务不能和当前正在执行任务以及后面待执
  // 行任务相同。
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
  	// 满足入队条件,将主任务入队
    queue.push(job)
    // 创建清队微任务
    queueFlush()
  }
}

queueFlush
创建微任务,isFlushingPending和isFlushing时表示微任务已创建等待执行或者正在执行微任务,这时候是会禁止再次创建更多的微任务,因为在主线程同步任务执行完后才会执行已创建的微任务,此时入队操作已完成,并且flushJobs会在一次微任务中会递归的将主任务队列全部清空,所以只需要一个微任务即可,如果重复创建微任务会导致接下来的微任务执行时队列是空的,那么这个微任务是无意义的,因为它不能清队。

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    // 微任务创建成功,并记录当前微任务,作为nextTick创建自定义微任务的支点,也就是说,
    // nextTick创建出来的微任务执行顺序紧跟在清队微任务后,保证自定义微任务执行时机的
    // 准确性
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

flushJob
isFlushingPending状态表示清队微任务已创建,此时js主线程还可能会有其他的同步任务未执行完,因此在主线程同步任务执行完毕前isFlushingPending一直为true,当flushJobs开始执行时,表明清队微任务开始执行,此时isFlushingPending置为false,isFlushing置为true,表示正在清队中。
flushJob大致顺序如下:
批量清空前置回调任务队列 -> 清空主任务队列 -> 批量清空后置回调任务队列

function flushJobs(seen?: CountMap) {
  // 关闭isFlushingPending微任务待执行标志位
  isFlushPending = false
  // 开启isFlushing清队中标志位
  isFlushing = true
  // 批量执行清空前置回调任务队列
  flushPreFlushCbs(seen)

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  // Jobs can never be null before flush starts, since they are only invalidated
  // during execution of another flushed job.
  // 将主任务队列中的任务按照ID进行排序,原因:1. 组件更新是由父到子的,而更新任务是在数据源
  // 更新时触发的,trigger会执行effect中的scheduler,scheduler回调会把effect作为更新
  // 任务推入主任务队列,排序保证了更新任务是按照由父到子的顺序进行执行;2. 当一个组件父组件
  // 更新时执行卸载操作,任务排序确保了已卸载组件的更新会被跳过
  queue.sort((a, b) => getId(a!) - getId(b!))

  try {
    // 遍历主任务队列,批量执行更新任务
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job) {
        // 执行当前更新任务
        // 注意:在isFlushing = true和isFlushing = false之间,
        // 主线程在批量执行更新任务(job),但是job中可能会引入新的
        // 更新任务入队,此时queue长度会变化,因此下面需要递归清空queue
        // 直到队列中的所有任务全部执行完毕
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 当前队列任务执行完毕,重置当前任务索引
    flushIndex = 0
    // 清空主任务队列
    queue.length = 0

	// 主队列清空后执行后置回调任务
    flushPostFlushCbs(seen)
	// 清队完毕,重置isFlushing状态值
    isFlushing = false
    // 当前清队微任务执行完毕,重置currentFlushPromise
    currentFlushPromise = null
    // 由于清队期间(isFlushing)也有可能会有任务入队,因此会导致按照实微任务开始执行时
    // 的队长度遍历清队,可能会导致无法彻底清干净。因此需要递归的清空队伍,保证一次清队
    // 微任务中所有任务队列都被全部清空
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值