解析nextTick---vue3任务调度

nextTick

定义:将回调推迟到下一个 DOM 更新周期之后执行,在更改了一些数据以等待 DOM 更新后立即使用它
在实际中使用这个方法一般是用于组件更新,你需要获取更新后的数据,所以使用nextTick等待DOM更新

// vue3中的语法对比vue2做了一些改动
import { createApp, nextTick } from 'vue'
const app = createApp({
  setup() {
    const message = ref('Hello!')
    const changeMessage = async newMessage => {
      message.value = newMessage
      // 这里的value是旧值
      await nextTick()
      // nextTick后获取的就是DOM更新后的value
      console.log('Now DOM is updated')
    }
  }
})
  • 这个api使用时相当简单,而且相当容易理解,但是为了知其意,还是要翻一下源码了解它的执行机制
export function nextTick(
  this: ComponentPublicInstance | void,
  fn?: () => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}
  • 上面是vue源码中nextTick的实现,为了搞清楚实现逻辑,就必须搞懂currentFlushPromise这个变量的含义,所以要从任务的调度机制开始分析

任务调度

首先这个调度机制的功能在runtime-corescheduler文件

  • API
// 这个文件会抛出以下几个API函数
nextTick(){} // 将函数在任务队列清空后执行
queueJob(){} // 添加任务并开始执行任务队列
invalidateJob(){} // 删除任务
queuePreFlushCb(){} // 添加前置回调函数并开始执行任务队列
queuePostFlushCb(){} // 添加后置回调函数并开始执行任务队列
flushPreFlushCbs(){} // 执行前置回调函数
flushPostFlushCbs(){} // 执行后置回调函数
  • 我们首先要知道几个关键变量
let isFlushing = false // 是否正在清空任务队列
let isFlushPending = false // 清队任务已创建,等待清空状态
const queue: SchedulerJob[] = [] // 任务队列
let flushIndex = 0 // 当前正在执行的任务在任务队列中的索引
  • 然后我们从queueJob这个函数开始
/* 
  这个函数主要是将一个任务(job)进行入队操作
  然后在满足条件的情况下启动清空队列任务(queueFlush)
 */
export function queueJob(job: SchedulerJob) {
  /**
   * 任务可入队逻辑
   * 1. 任务队列为空
   * 2. 待入队任务不能存在于任务队列中(按情况分析)
   */
  if (
    (!queue.length ||
      !queue.includes(
        job,
        /* 
          在正在清空队列且当前待入队任务是可以递归时,
          说明当前任务一定和当前正在执行任务是同一任务,所以+1,
          就是为了保证待入队任务和正在执行任务相同,但不能和后面待执行任务相同
         */
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    // 二分查找任务在队列中的位置
    const pos = findInsertionIndex(job)
    if (pos > -1) {
      queue.splice(pos, 0, job)
    } else {
      queue.push(job)
    }
    queueFlush()
  }
}
  • queueFlush
function queueFlush() {
  /**
    清队任务创建后禁止再次创建更多的清队任务
    因为在入队操作完成后,flushJobs会在一次递归中将任务队列全部清空,所以只需要一次清队任务即可
   */
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    /* 
      清队任务创建成功,并记录下当前清队任务,这个标记可以用于nextTick创建自定义函数,
      说明nextTick的执行时机是在清队任务后的,其实从这个地方就可以理解nextTick的执行原理了
    */
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
  • flushJobs
// 清空任务队列
function flushJobs(seen?: CountMap) {
  isFlushPending = false // 关闭清队任务等待状态
  isFlushing = true // 开启正在清空队列状态
  if (__DEV__) {
    seen = seen || new Map()
  }

  // 清空前置回调任务队列
  flushPreFlushCbs(seen)

  /* 
    任务队列中的任务根据ID进行排序的原因
      1. 因为组件更新是从父组件到子组件的,而任务更新是在数据源更新时触发的,所以为了更新任务的顺序就需要进行排序
      2. 如果在父组件更新期间已经卸载了组件,那么子组件的更新任务就可以跳过
  */
  queue.sort((a, b) => getId(a) - getId(b))

  try {
    // 遍历任务队列
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
          continue
        }
        // 执行当前任务
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 重置当前任务索引
    flushIndex = 0
    // 清空任务队列
    queue.length = 0

    // 执行后置回调任务队列
    flushPostFlushCbs(seen)
	// 重置清队任务的状态
    isFlushing = false
    currentFlushPromise = null
    /* 
      因为清队任务执行期间也会有任务入队,所以为了清队执行完成
      就需要判断各任务队列的长度,然后递归执行
    */
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

总结

  • nextTick的执行时机是在任务队列(前置、主任务、后置)清除后的,currentFlushPromise是清队任务的promise标记
  • 任务队列执行顺序:执行前置回调任务队列 -> 执行主任务队列 -> 执行后置回调任务队列
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值