关于前端:Vue3-任务调度器-scheduler-源码分析

8次阅读

共计 6575 个字符,预计需要花费 17 分钟才能阅读完成。

scheduler任务调度器

前置铺垫:schedler的源码尽管只有二百多行,并且与组件更新前后、更新中的所有执行的【工作】无关。【工作】在这里有比拟形象,了解起来比拟艰难。好在有一点就是咱们在上一篇的文章中有了解到一个工作:watch Effect。咱们就能够联合这个 job,对scheduler 进行剖析。在后续的文章中,当咱们解说到 update 阶段的时候,会回来再看下scheduler。到时候就能明确不少啦。

上面间接进入正题:

调度器在执行工作的过程中,次要将工作分为三个阶段,每个阶段两种状态:

  • 前置刷新阶段
  • 刷新阶段
  • 后置刷新阶段

每个阶段各有两种状态:

  • 正在期待刷新
  • 正在刷新

每次刷新的时候,通过 Promise.resolve 启动一个微工作,调用flushJo b 函数,先进行前置刷新工作,直至前置回调工作池为空,在刷新当前任务队列,当前任务队列刷新完结,最初刷新后置回调工作池,如此周而复始,直至三个工作池中的回调都刷新完结。

在解说 watch 的时候,咱们说过,watch effect会在组件 update 之前执行。这与用户定义的副作用函数 配置项 fulsh 无关。

  • flush: pre,默认值。watch Effectflush 就是 pre

    • 在创立 watch 的时候通过调用 queuePreFlushCb(job),将副作用函数pushpendingPreFlushCbs
    • 当组件须要进行 update 的时候,会先遍历执行 pendingPreFlushCbs 池中的回调
    • 从而做到在组件 update 前进行刷新。
  • fulsh: post。可选 但不举荐

    • 当设置 watch effectflushpost 的时候就会调用 queuePostFlushCb 函数,将副作用函数 pushpendingPostFlushCbs
    • 当 queue 中的工作执行完之后,就会遍历执行 pendingPostFlushCbs 中的工作
    • 从而做到在组件 update 后进行刷新

上面咱们一起看下这块相干的代码:

// 前置更新相干
const pendingPreFlushCbs = []
let activePreFlushCbs = null
let preFlushIndex = 0

// 后置更新相干
const pendingPostFlushCbs  = []
let activePostFlushCbs = null
let postFlushIndex = 0
function queuePreFlushCb(cb) {queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

function queuePostFlushCb(cb) {queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

function queueCb(cb, activeQueue, pendingQueue, index) {if (!isArray(cb)) {
    // cb 不是数组
    if (
      !activeQueue ||
      !activeQueue.includes(
        cb,
        (cb as SchedulerJob).allowRecurse ? index + 1 : index
      )
    ) {
      // activeQueue 不存在 || 从 index+ 1 地位开始 activeQueue 不蕴含 cb
      // watch job 会进来
      pendingQueue.push(cb)
    }
  } else {

    // 如果 cb 是一个数组,则它是一个组件生命周期挂钩,只能由一个作业触发,// 该作业已在主队列中打消反复,sowe 能够在此处跳过反复查看以进步性能
    pendingQueue.push(...cb)
  }
  queueFlush()}

从下面的代码中能够复制往各阶段工作池中,push工作的次要是 queueCb 函数,queueCb函数次要负责对工作进行判断,当工作是数组时,会间接解构至待执行队列中,当工作非数组的时候,须要对工作进行判断,push 的工作不能在正在执行的工作队列中存在,或者以后没有正在执行的工作队列。最初会调用 queueFlush 函数。

queueFlush函数会依据以后的状态进行判断,只有非正在刷新且非正在期待刷新的状态下。才会通过 Promise.resolve 启动微工作,刷新队列。

看下 queueFlush 的代码:

// 冲刷队列
function queueFlush() {
  // 如果没有正在刷新的 && 正在期待刷新的
  // 则执行 flushJobs
  if (!isFlushing && !isFlushPending) {
    // 正在期待刷新
    isFlushPending = true
    // 启动微工作,开始刷新工作队列。// flushJobs 执行完结 将 promise 赋值给 currentFlushPromise
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

当启动微工作刷新队列的时候,会将 isFlushPending = true,示意开始期待刷新。当以后宏工作执行完结后,会执行相应的微工作队列,这时就会调用flushJobs 函数。开始刷新队列。

以后宏工作有哪些,咱们先不关注。首先要晓得每个宏工作都会对应一个微工作队列,宏工作执行完结才会执行相应的微工作队列。

这也就是 Vue 所提到的【防止同一个“tick”中多个状态扭转导致的不必要的反复调用,并异步刷新用户副作用函数】

function flushJobs(seen?: CountMap) {
  // 👉 期待刷新完结,开始刷新
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {seen = seen || new Map()
  }

  // 👉 前置刷新开始 jobs
  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.
  // 👉 在刷新前对队列排序
  // 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}
        // 执行 job 函数
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 👉 重置正在刷新队列
    flushIndex = 0
    queue.length = 0

    // 👉 刷新后置刷新 jobs
    flushPostFlushCbs(seen)
    // 👉 刷新完结
    isFlushing = false
      
    // 重置以后刷新的 promise
    // 最初再 nextTick 中会用到
    currentFlushPromise = null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    // 👉 如果还有当前任务或者,期待的估算新工作,或者期待的后刷新工作,则递归刷新
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      // 递归刷新
      flushJobs(seen)
    }
  }
}

flushJobs函数就是切入口,次要负责所有工作队列的刷新工作,前置工作的刷新次要是在该函数中调起 flushPreFlushCbs(seen) 函数,先去刷新前置工作池中的所有工作。

flushPreFlushCbs(seen) 函数代码:

export function flushPreFlushCbs(seen ,parentJob) {if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob
    // 👉 去重
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    // 👉  置预刷 jobs array 为空
    pendingPreFlushCbs.length = 0
    if (__DEV__) {seen = seen || new Map()
    }
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        // 递归刷新查看
        continue
      }
      // 👉 执行 job eg: watch job
      // 👉 watch 会在这里执行
      activePreFlushCbs[preFlushIndex]()}
    // 👉 重置
    activePreFlushCbs = null
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // recursively flush until it drains
    // 👉 递归刷新预刷新 jobs
    flushPreFlushCbs(seen, parentJob)
  }
}

对于 flushPreFlushCbs 函数,咱们把次要关注点先放在:

  • 前置更新状态的切换,由pendingactive
  • 遍历执行前置工作池中的每个工作
  • 当遍历完结会重置以后状态及index
  • 递归调用 flushPreFlushCbs,直至pendingPreFlushCbs 工作池为空。
  • 次要是保障所有正在期待的队列会被执行到

有的同学可能会有疑难:既然曾经 通过 pendingPreFlushCbs.length = 0,将待执行工作池清空了,为什么还须要递归持续。

这个其实与遍历执行的工作无关,有的工作中,还会持续创立待执行工作,这时就会将创立的待执行工作持续 push 至待执行工作池。故须要递归遍历执行

flushPreFlushCbs 函数执行完结后,就会进行以后遍历。即进入了 flushing 阶段,这时存在于 queueupdate函数就会执行,组件就会进行更新。然而在执行 queue 中的工作的时候,须要对工作去重 排序,这些工作实现之后,才会遍历执行 queue 中的工作。

queue 中的工作执行完结后,会通过 flushIndex = 0queue.length = 0,对以后队列进行重置。

随后就会调用 flushPostFlushCbs 函数,该函数会刷新后置刷新队列,同样的主逻辑:扭转后置刷新阶段状态,遍历执行后置刷新阶段工作池中的所有工作。

watch Effect flush: post的时候,这时就会遍历执行到watch effect

flushPostFlushCbs与后面两个函数不一样的是:没有进行递归刷新。次要目标是为了保障各阶段中工作能按:前置➡以后➡后置 阶段的程序进行刷新!

flushPostFlushCbs函数的代码:

export function flushPostFlushCbs(seen?: CountMap) {
  // 👉 如果存在后置刷新工作
  if (pendingPostFlushCbs.length) {
    // 👉 去重 job
    const deduped = [...new Set(pendingPostFlushCbs)]
    // 👉 正在期待的工作池 状况
    pendingPostFlushCbs.length = 0

    // 👉 #1947 already has active queue, nested flushPostFlushCbs call
    if (activePostFlushCbs) {
      // 👉 如果曾经有沉闷的队列,嵌套的 flushPostFlushCbs 调用
      activePostFlushCbs.push(...deduped)
      return
    }
    // 👉 将期待的作为以后的工作
    activePostFlushCbs = deduped
    if (__DEV__) {seen = seen || new Map()
    }
    // 👉 对后置工作进行排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {continue}
      //👉  执行后置工作
      activePostFlushCbs[postFlushIndex]()}
    // 👉 重置正在执行的工作池
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

flushPostFlushCbs 函数执行完结的时候,就会回到 flushJobs 函数,通过 isFlushing = false 重置刷新状态。

最初通过各个阶段工作池中时候有工作,再持续递归调用 flushJobs 函数。

如此往返,直至所有阶段的工作执行完结。

nextTick原理

咱们晓得nextTick API 会将回调提早到下次 DOM 更新循环之后执行。并会返回一个Promise

通过理解 flushJobs 函数,flushJobs函数次要就是通过 Promsie.resolve 执行的,当 flushJobs 函数执行完结,也就是 Promsie.resolve 更改状态的时候。

首先 flushJobs 函数会置空 currentFlushPromise。最初才会通过 Promsie.resolve 赋值给currentFlushPromise

当调用 nextTick 的时候,返回的promise,其实就是currentFlushPromise

能够再下来看下 flushJobs 函数中的代码。

nextTick代码:

const resolvedPromise: Promise<any> = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null

function nextTick(
  this: ComponentPublicInstance | void,
  fn?: () => void): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

其实明确了 scheduler 的调度过程,nextTick很好了解。

总结

通过剖析咱们晓得在 Vue3scheduler任务调度器,在执行工作的过程中,次要分为三个阶段,前置刷新阶段、后置刷新阶段、以后刷新阶段(update 阶段),每个阶段都有两种状态:期待刷新 & 正在刷新 ,每个阶段发生变化后,状态都会进行重置。并且是按 前置➡以后➡后置➡前置 …的过程进行的,如此往返,直到各阶段工作池中的所有工作完结。nextTick是等所有阶段的刷新工作完结后返回的一个Promise.resolve

最初上一张图,总结下整个过程。

最初还是很 (bu) 真(yao)诚 (lian) 的举荐下我的公众号:coder 狂想曲。您的关注就是对我创作的最大激励呐。

正文完
 0