Vue Scheduler

Vue Scheduler

Vue中的Scheduler(任务调度系统),主要责任就是收集Vue在程序执行过程中产生的Jobs(任务),让每个Job按照固定的顺序执行,避免资源的浪费和程序中的循环依赖导致的死循环。

Vue Scheduler中的核心分为以下几个部分:

  • 任务队列(queue、pendingPostFlushCbs、activePostFlushCbs)
  • 任务入队程序(queueJob, queuePostFlushCb)
  • 任务执行状态(isFlushing、isFlushPending)
  • 当前正在执行的任务Promise(currentFlushPromise)
  • 任务执行程序(flushJobs、flushPostFlushCbs、flushPreFlushCbs)
  • 任务优先级调配程序(comparator)

源码位置:packages/runtime-core/src/scheduler.ts

任务队列

Vue Scheduler中的所有任务分为pre普通任务post三种显式优先级,以及根据id进行排序的隐式优先级(主要用作将父组件任务前置,确保组件的任务链不会乱序),如下所示:

  • pre任务普通任务放在queue
  • post任务放在pendingPostFlushCbs以及activePostFlushCbs
flowchart TB subgraph queue pre任务 普通任务 end subgraph pendingPostFlushCbs post任务 end subgraph activePostFlushCbs 正在执行的post任务 end

任务的添加

在Vue执行的不同阶段都有任务产生,一部分任务会根据当时的配置立即执行或某些条件下随parent.update执行,另一部分会被添加到任务调度系统中进行统一的调度,以下为Vue中的入队方法:

ts 复制代码
// packages/runtime-core/src/scheduler.ts
export function queueJob(job: SchedulerJob) {
  if (
    !queue.length ||
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
    )
  ) {
    if (job.id == null) {
      queue.push(job)
    } else {
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}

export function queuePostFlushCb(cb: SchedulerJobs) {
  if (!isArray(cb)) {
    if (
      !activePostFlushCbs ||
      !activePostFlushCbs.includes(
        cb,
        cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
      )
    ) {
      pendingPostFlushCbs.push(cb)
    }
  } else {
    pendingPostFlushCbs.push(...cb)
  }
  queueFlush()
}

queueJob

在任务进入queueJob时会进行查重的判断,不存在才会将任务push或insert进queue,同时由watch进入的Job是允许递归调用自身的,这时需要开发者自己思考好自己的代码是否会无限执行。

  • 在renderer.ts的setupRenderEffect函数(处理组件渲染的核心函数)中会创建update对象,在update执行时任务会被收集到queue中,如下:

    ts 复制代码
    // packages/runtime-core/src/renderer.ts
    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))
    // ...
    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // ...
    
    update()
  • this.$forceUpdate()执行时会被放入queue中,如下为Vue在this上拓展的属性其中就包含$forceUpdate

    ts 复制代码
    // packages/runtime-core/src/componentPublicInstance.ts
    export const publicPropertiesMap: PublicPropertiesMap =
      // Move PURE marker to new line to workaround compiler discarding it
      // due to type annotation
      /*#__PURE__*/ extend(Object.create(null), {
        $: i => i,
        $el: i => i.vnode.el,
        $data: i => i.data,
        $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props),
        $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs),
        $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots),
        $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs),
        $parent: i => getPublicInstance(i.parent),
        $root: i => getPublicInstance(i.root),
        $emit: i => i.emit,
        $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
        $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
        $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
        $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
      } as PublicPropertiesMap)
  • 异步组件加载时

    ts 复制代码
    // packages/runtime-core/src/apiAsyncComponent.ts
    // ...
    load()
    .then(() => {
      loaded.value = true
      if (instance.parent && isKeepAlive(instance.parent.vnode)) {
        // parent is keep-alive, force update so the loaded component's
        // name is taken into account
        queueJob(instance.parent.update)
      }
    })
    .catch(err => {
      onError(err)
      error.value = err
    })
    // ...
  • Watch执行时

    ts 复制代码
    // packages/runtime-core/src/apiWatch.ts
    // ...
    let scheduler: EffectScheduler
    if (flush === 'sync') {
      scheduler = job as any // the scheduler function gets called directly
    } else if (flush === 'post') {
      scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
    } else {
      // default: 'pre'
      job.pre = true
      if (instance) job.id = instance.uid
      scheduler = () => queueJob(job)
    }
    // ...
  • hmr(开发环境热更新)时也会执行queueJob

queuePostFlushCb

在Job进入queuePostFlushCb时也会进行判断,此时判断的是任务是否在activePostFlushCbs队列中,即是否将要执行,如果不存在即将执行的任务队列则添加到pendingPostFlushCbs队列

  • 在组件的mount、update、unmount等生命周期时都会执行queuePostFlushCb添加后置任务

    点太多了,都粘过来有点多余了,具体详见源码: packages/runtime-core/src/renderer.ts

  • Suspense组件中父组件全部加载完之后

    ts 复制代码
    // packages/runtime-core/src/components/Suspense.ts
    // ...
    // flush buffered effects
    // check if there is a pending parent suspense
    let parent = suspense.parent
    let hasUnresolvedAncestor = false
    while (parent) {
      if (parent.pendingBranch) {
        // found a pending parent suspense, merge buffered post jobs
        // into that parent
        parent.effects.push(...effects)
        hasUnresolvedAncestor = true
        break
      }
      parent = parent.parent
    }
    // no pending parent suspense, flush all jobs
    if (!hasUnresolvedAncestor) {
      queuePostFlushCb(effects)
    }
  • hmr(热更新之后)

    ts 复制代码
    // packages/runtime-core/src/hmr.ts
    // 5. make sure to cleanup dirty hmr components after update
    queuePostFlushCb(() => {
      for (const instance of instances) {
        hmrDirtyComponents.delete(
          normalizeClassComponent(instance.type as HMRComponent)
        )
      }
    })

ssr 相关的没列

任务优先级

Vue中的scheduler通过comparator给queue队列中的pre任务普通任务进行排序,而pendingPostFlushCbs队列则存放post任务一定在queue队列执行之后才会执行

ts 复制代码
// packages/runtime-core/src/scheduler.ts
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
  const diff = getId(a) - getId(b)
  // 如果id相同根据是否是pre进行排序,pre优先于普通任务, 如果id不同则根据从小到大排序,确保parent的任务优于children的任务执行
  if (diff === 0) {
    if (a.pre && !b.pre) return -1
    if (b.pre && !a.pre) return 1
  }
  return diff
}
flowchart LR a(pre Job1) --> a2(Job1) --> a3(Job2) --> a4(post Job1)

任务执行分析

任务通过以下三个函数执行:

flushJobs

这个函数最初由queueJob开始触发执行queueFlush,包装一层Promise.resolve后执行flushJobs

此处主要分析flushJobs函数,其余源码请看:packages/runtime-core/src/scheduler.ts

ts 复制代码
function flushJobs(seen?: CountMap) {
  // 设置任务状态为Flushing(已完成)
  isFlushPending = false
  isFlushing = true
  // ...
  queue.sort(comparator)
  // ...
  try {
    // 此处按优先级执行任务Job
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // 判断Job是否失活,比如任务执行前卸载了组件,就会跳过这个任务
      if (job && job.active !== false) {
        // ...
        // 由统一的错误处理程序执行job,可以统一报错
        // 具体声明详见: packages/runtime-core/src/errorHandling.ts
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 将标志及队列初始化
    flushIndex = 0
    queue.length = 0
    // 执行后置任务
    flushPostFlushCbs(seen)
    // 初始化任务状态及任务当前的Promise
    isFlushing = false
    currentFlushPromise = null
    // 如果queue或pendingPostFlushCbs中还存在任务则递归调用当前函数
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}

flushPreFlushCbs

这个函数只会在组件render和updateComponentPreRender时才会执行

ts 复制代码
export function flushPreFlushCbs(
  seen?: CountMap,
  // if currently flushing, skip the current job itself
  i = isFlushing ? flushIndex + 1 : 0
) {
  // ...
  // 从标识位开始执行,标识位之前的是已经执行过的
  for (; i < queue.length; i++) {
    const cb = queue[i]
    // 如果pre属性存在则为前置任务,执行完删除,同时标志位向前挪一位(不挪会导致少执行一个任务)
    if (cb && cb.pre) {
      // ...
      queue.splice(i, 1)
      i--
      cb()
    }
  }
}

flushPostFlushCbs

这个函数会执行后置任务即post任务,除在flushJobs中执行完queue队列后执行外,在组件render阶段也会被执行

ts 复制代码
export function flushPostFlushCbs(seen?: CountMap) {
  // 判断post任务的队列是否有任务
  if (pendingPostFlushCbs.length) {
    // 进行去重,同时清空post队列
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0

    // 判断是否存在正在执行的后置任务队列即activePostFlushCbs队列,存在则直接加到队列后边
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }
    // 将pendingPostFlushCbs队列内的任务去重后赋值给正在执行的队列activePostFlushCbs队列
    activePostFlushCbs = deduped
    // ...
    // 根据id排序,确保parent任务优先执行
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
    // 按顺序执行任务
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // ... 
      activePostFlushCbs[postFlushIndex]()
    }
    // 初始化正在执行的后置任务队列activePostFlushCbs
    // 初始化后置任务标志位
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

nextTick

作为Vue中的一个全局API,它的作用是等待下一次 DOM 更新刷新的工具方法.

API介绍: cn.vuejs.org/api/general...

它同样写在了packages/runtime-core/src/scheduler.ts文件中,它是通过什么才能确保在下一次更新时才执行回调的呢,是不是有这个疑问,下面就是它的源码:

ts 复制代码
export function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R
): Promise<Awaited<R>> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

它就是在任务调度系统中维护的那个currentFlushPromise当前任务的Promise之后加了个.then的微任务,由于任务队列中的任务都会在currentFlushPromise的第一个.then中执行,所以nextTick才能在那些任务之后执行。

总结

scheduler的作用就是接收其他的函数丢过来的任务,然后把他们执行掉

别人只管丢任务给总部,不用管任务什么时候做,就好比我们的排期,产品只管丢给actionView,actionView会安排任务什么时候执行。。

相关推荐
老华带你飞1 小时前
畅阅读小程序|畅阅读系统|基于java的畅阅读系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·小程序·毕设·畅阅读系统小程序
小熊学Java2 小时前
基于 Spring Boot+Vue 的高校竞赛管理平台
vue.js·spring boot·后端
百思可瑞教育6 小时前
uni-app 根据用户不同身份显示不同的tabBar
vue.js·uni-app·北京百思可瑞教育·北京百思教育
华仔啊9 小时前
Vue3 的 ref 和 reactive 到底用哪个?90% 的开发者都选错了
javascript·vue.js
IT古董12 小时前
Vue + Vite + Element UI 实现动态主题切换:基于 :root + SCSS 变量的最佳实践
vue.js·ui·scss
百思可瑞教育14 小时前
使用UniApp实现一个AI对话页面
javascript·vue.js·人工智能·uni-app·xcode·北京百思可瑞教育·百思可瑞教育
不想吃饭e15 小时前
在uniapp/vue项目中全局挂载component
前端·vue.js·uni-app
知识分享小能手18 小时前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
朗迹 - 张伟19 小时前
Gin-Vue-Admin学习笔记
vue.js·学习·gin