深度解析 Vue3 的调度系统

发表于 2年以前  | 总阅读数:1002 次

前言

什么是调度?

调度这一概念最开始应该来自于操作系统。

由于计算机资源的有限性,必须按照一定的原则,选择任务来占用资源。

操作系统引入调度,目的是解决计算机资源的分配问题,因为任务是源源不断的,但 CPU 不能同时执行所有的任务。如:对部分优先级高的任务(如:用户交互需要立即反馈),需要先占用资源/ 运行,这就是一个优先级的调度。

Vue 的调度是什么?有什么不同?

Vue 的调度,行为上也是按照一定的原则,选择任务来占用资源/执行。但同样的行为,目的却是不一样的。

因为,Vue 并不需要解决计算机资源分配的问题(操作系统解决)。Vue 利用调度算法,保证 Vue 组件渲染过程的正确性以及 API 的执行顺序的正确性(不好理解的话可以先看下文)

在 Vue3 的 API 设计中,存在着各种的异步回调 API 设计,如:组件的生命周期,watch API 的回调函数等。这些回调函数,并不是立即执行的,它们都作为任务(Job),需要按照一定的规则/顺序去执行

部分规则如下:

  • watch 的 callback 函数,需要在组件更新前调用
  • 组件 DOM 的更新,需要在响应式数据(Vue 模板依赖的 ref、reactive、data 等数据的变化)更新之后
  • 父组件需要先更新,子组件后更新
  • Mounted 生命周期,需要在组件挂载之后执行
  • updated 生命周期,需要在组件更新之后执行
  • ……

Vue 的 API 设计中就制定了这份规则,在什么时候应该执行什么任务,而这个规则在代码中的实现,就是调度算法

学习 Vue 调度的目的

Vue 不是调度算法发明者,相反,Vue是调度算法的使用者和受益者。这些设计,都是基于先人的探索沉淀,再结合自身需求改造出来的。

前端技术的更新迭代速度非常快,但是这些优秀的设计,却是不变的,这也就是我们学习这些优秀设计的目的,能够做到,以不变应万变。

调度算法基本介绍

调度算法有两个基本数据结构:队列(queue),任务(Job)

image-20220116115525759

  • 入队:将任务加入队列,等待执行
  • 出队:将任务取出队列,立即执行

调度算法有很多种,它们都有不同的目的,但它们的基本数据结构都相同,不同点在于入队和出队的方式

下面是两种常见的调度算法

  • 先来先服务(FCFS):先入队的 Job 先执行。这种算法常见于,Job 平等、没有优先级的场景。
  • 优先级调度算法:优先级高的 Job 先执行。

image-20220117220918349

调度算法里面一点关于 Vue 的东西都没有,如何跟 Vue 扯上关系?

调度算法是对整个调度过程的抽象,算法无需关心任务(Job)的内容是什么,它作为 Vue3 的一种基础设施,起到了解耦的作用(如果暂时还理解不了这句话,下一小节还有解释)

调度算法只调度执行的顺序,不负责具体的执行

那么 Vue 是如何利用调度算法,来实现自身 API 的正确调度的呢?我们在文章后面会详细描述

Vue3 调度算法的使用

Vue3 的调度算法,与上面提到的算法,大致相同,只是适配了 Vue 的一些细节

Vue 有 3 个队列,分别为:

  • 组件 DOM 更新(不是组件的数据 data 更新)前队列,后面也称为 Pre 队列
  • 组件 DOM 更新(不是组件的数据 data 更新)队列,后面也称为 queue 队列 / 组件异步更新队列
  • 组件 DOM 更新(不是组件的数据 data 更新)后队列,后面也称为 Post 队列

image-20220116201444970

3 个队列的部分特性对比(大概看看即可,后面会详细介绍):

Pre 队列 queue 队列 Post 队列
队列作用 执行组件 DOM 更新之前的任务 执行组件 DOM 更新 执行组件 DOM 更新之后的任务
出队方式 先进先出 允许插队,按 id 从小到大执行 允许插队,按 id 从小到大执行

整个调度过程中,只有入队过程,是由我们自己控制,整个队列的执行(如何出队),都由队列自身控制

因此:调度算法对外暴露的 API,也只有入队 API:

  • queuePreFlushCb:加入 Pre 队列
  • queueJob:加入 queue 队列
  • queuePostFlushCb:加入 Post 队列

下面是用法:

const job1 = () => {
    // 假设这里是父组件的 DOM 更新逻辑
    console.log('父组件 DOM 更新 job 1')
}
job1.id = 1  // 设置优先级,Vue 规定是 id 越小,优先级越高

const job2 = () => {
    // 假设这里是子组件的 DOM 更新逻辑
    console.log('子组件 DOM 更新 job 2')
}
job2.id = 2  // 设置优先级

// 加入 queue 队列
// job 2 先加入,但是会在 job 1 之后执行,因为 id 小的,优先级更高
queueJob(job2)
queueJob(job1)

// 加入 Post 队列
queuePostFlushCb(() => {
    // 假设这里是 updated 生命周期
    console.log('执行 updated 生命周期 1')
})
// 加入 Post 队列
queuePostFlushCb(() => {
    // 假设这里是 updated 生命周期
    console.log('执行 updated 生命周期 2')
})

// 加入 Pre 队列
queuePreFlushCb(() => {
    // 假设这里是 watch 的回调函数
    console.log('执行 watch 的回调函数 1')
})
// 加入 Pre 队列
queuePreFlushCb(() => {
    // 假设这里是 watch 的回调函数
    console.log('执行 watch 的回调函数 2')
})
console.log('所有响应式数据更新完毕')

打印结果如下:


// 所有响应式数据更新完毕
// 执行 watch 的回调函数 1
// 执行 watch 的回调函数 2
// 父组件 DOM 更新 job 1
// 子组件 DOM 更新 job 2
// 执行 updated 生命周期 1
// 执行 updated 生命周期 2

队列使用上非常的简单,只要往对应的队列,传入 job 函数即可。队列会在当前浏览器任务的所有 js 代码执行完成后,才开始依次执行 Pre 队列、queue 列、Post 队列

调度算法是对整个调度过程的抽象

这里我们应该能更好的理解这句话,队列只是根据其自身的队列性质(先进先出 or 优先级),选择一个 Job 执行,队列不关心 Job 的内容是什么。

这样的设计,可以极大的减少 Vue API 和 队列间耦合,队列不知道 Vue API 的存在,即使 Vue 未来新增新的异步回调的 API,也不需要修改队列。

在上述例子中:我们大概可以看出,Vue3 是如何使用调度 API,去控制各种类型的异步回调的执行时机的。对于不同的异步回调 API,会根据 API 设计的执行时机,使用不同的队列

如:

  • watch 的回调函数,默认是在组件 DOM 更新之前执行,因此使用 Pre 队列。
  • 组件 DOM 更新,使用 queue 队列。
  • updated 生命周期需要在组件 DOM 更新之后执行,因此使用的是 Post 队列。

image-20220117221400312

本文不会过多的介绍 Job 的具体内容的实现(不同的 API,Job 的内容都是不一样的),而是专注于调度机制的内部实现,接下来我们的深入了解 Vue 的调度机制内部。

名词约定

我们从一个例子中,理解用到的各种名词:

<template>
  <div>{{count}}</div>
  <button @click='add'>Add</button>
</template>
<script setup lang='ts'>
import { ref } from 'vue'

const count = ref(0)

function add() {
  count.value = count.value + 1  // template 依赖 count,修改后会触 queueJob(instance.update)
}
</script>

响应式数据更新

指模板依赖的 ref、reactive、组件 data 等响应式数据的变化

这里指点击按钮触发的 click 回调中,响应式数据 count.value 被修改

组件 DOM 更新

实际上是调用 instance.update 函数,该函数会对比组件 data 更新前的 VNode组件 data 更新后的 VNode,对比之间的差异,修改差异部分的 DOM。该过程叫 patch,比较 vnode 的方法叫 diff 算法(因为这里没有篇幅展开,因此大概看看记住 instance.update 的特点即可)

  • instance 是指 Vue 内部的组件实例,我们直接使用接触不到该实例。
  • instance.update深度更新,即除了会更新组件本身,还会递归调用子组件的 instance.update ,因此,这个过程会更新整个组件树。
  • instance.update更新该组件的属性(如果父组件的传入发生变化),然后更新它对应的 DOM
  • **响应式数据更新 ≠ 组件 DOM **更新,响应式数据更新,只是变量值的改变,此时还没修改 DOM,但会立即执行 queueJob(instance.update),将组件 DOM 更新任务,加入到队列。即数据修改是立即生效的,但 DOM 修改是延迟执行

image-20220116215351750

调度细节

用一个表格总结 3 个调度过程中的一些细节

Pre 队列 queue 队列 Post 队列
队列作用 执行组件 DOM 更新之前的任务 执行组件 DOM 更新 执行组件 DOM 更新之后的任务
任务去重 去重 去重 去重
出队方式 先进先出 允许插队,按 id 从小到大执行 允许插队,按 id 从小到大执行
任务有效性 任务全部有效 组件卸载时,对应的任务失效 任务全部有效
删除任务 不需要 特殊情况需要删除任务 不需要
Job 递归 默认允许 默认允许 默认允许

接下来我们一个个细节进行解析:

任务去重

每次修改响应式变量(即修改相应的响应式数据),都会将组件 DOM 更新 Job加入队列。

// 当组件依赖的响应式变量被修改时,会立即调用 queueJob
queueJob(instance.update)

那当我们同时修改多次,同一个组件依赖的响应式变量时,会多次调用 queueJob。

下面是一个简单的例子:

<template>
  <div>{{count}}</div>
  <button @click='add'>Add</button>
</template>
<script setup lang='ts'>
import { ref } from 'vue'

const count = ref(0)

function add() {
  count.value = count.value + 1  // template 依赖 count,修改后会触 queueJob(instance.update)
  count.value = count.value + 2  // template 依赖 count,修改后会触 queueJob(instance.update)
}
</script>

count.value 前后两次被修改,会触发两次 queueJob

为了防止多次重复地执行更新,需要在入队的时候,对 Job 进行去重(伪代码):

export function queueJob(job: SchedulerJob) {
  // 去重判断
  if (!queue.includes(job)) {
   // 入队
    queue.push(job)
  }
}

其他队列的入队函数也有类似的去重逻辑。

优先级机制

只有 queue 队列和 Post 队列,是有优先级机制的,job.id 越小,越先执行

为什么需要优先级队列?

queue 队列和 Post 队列使用优先级的原因各不相同。

我们来逐一分析:

queue 队列的优先级机制

queue 队列的 Job,是执行组件的 DOM 更新。在 Vue 中,组件并不都是相互独立的,它们之前存在父子关系

必须先更新父组件,才能更新子组件,因为父组件可能会传参给子组件(作为子组件的属性)

下图展示的是,父组件和子组件及其属性更新先后顺序:

image-20220121121340442

父组件 DOM 更新前,才会修改子组件的 props,因此,必须要先执行父组件 DOM 更新,子组件的 props 才是正确的值。

因此:父组件优先级 > 子组件优先级

如何保证父组件优先级更高?即如何保证父组件的 Job.id 更小?

我们上一小节说过,组件 DOM 更新,会深度递归更新子组件。组件创建的过程也一样,也会深度递归创建子组件。

下面是一个组件树示意图,其创建顺序如下:

image-20220117210509918

深度创建组件,即按树的深度遍历的顺序创建组件。深度遍历,一定是先遍历父节点,再遍历子节点

因此,从图中也能看出,父组件的序号,一定会比子组件的序号小,使用序号作为 Job.id 即可保证父组件优先级一定大于子组件

这里我们可以感受一下深度遍历在处理依赖顺序时的巧妙作用,前辈们总结出来的算法,竟有如此的妙用。

我们学习源码,学习算法,就是学习这些设计。

当我们以后在项目中,遇到依赖谁先执行的问题,会想起深度遍历这个算法。

要实现 queue 队列 Job 的优先级,我们只需要实现插队功能即可:(伪代码):

export function queueJob(job: SchedulerJob) {
  // 去重判断
  if ( !queue.includes(job) ) {
    // 没有 id 放最后
    if (job.id == null) {
      queue.push(job)
    } else {
      // 二分查找 job.id,计算出需要插入的位置
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
  }
}

Post 队列的优先级机制

先回顾一下我们常常使用到的 Post 队列的 Job,都有哪些:

  • mounted、updated 等生命周期,它们有个共同特点,就是需要等 DOM 更新后,再执行
  • watchPostEffect API,用户手动设置 watch 回调在 DOM 更新之后执行

这些用户设定的回调之间,并没有依赖关系

那为什么 Post 队列还需要优先级呢?

因为有一种内部的 Job,要提前执行,它的作用是,更新模板引用

因为用户编写的回调函数中,可能会使用到模板引用,因此必须要在用户编写的回调函数执行前,把模板引用的值更新

看如下代码:

<template>
  <button @click='add' >count: {{ count }}</button>
  <div v-if="count % 2" :ref="divRef">count 为偶数</div>
  <div v-else :ref="divRef">count 为奇数</div>
</template>
<script setup lang='ts'>
import {onUpdated, ref} from 'vue'

const count = ref(0)

function add() {
  count.value = count.value + 1
}
const divRef = ref<HTMLElement>()

onUpdated(() => {
  console.log('onUpdated', divRef.value?.innerHTML)
})
</script>

响应式变量 count 为奇数或偶数时,divRef.value 指向的 DOM 节点是不一样的。

必须要在用户写的 updated 生命周期执行前,先更新 divRef,否则就会取到错误的值。

因此,更新模板引用的 Job,job.id = -1,会先执行

而其他用户设定的 job,没有设置 job.id,会加入到队列末尾,在最后执行。

失效任务

当组件被卸载(unmounted)时,其对应的 Job 会失效,因为不需要再更新该组件了。失效的任务,在取出队列时,不会被执行。

只有 queue 队列的 Job,会失效。

下面是一个失效案例的示意图:

image-20220121123048216

  1. 点击按钮,count.value 改变
  2. count 响应式变量改变,会立即 queueJob 将子组件 Job 加入队列
  3. emit 事件,父组件 hasChild.value 改变
  4. hasChild 响应式变量改变,会立即 queueJob 将父组件 Job 加入队列
  5. 父组件有更高优先级,先执行。
  6. 更新父组件 DOM,子组件由于 v-if,被卸载
  7. 子组件卸载时,将其 Job 失效,Job.active = false

要实现失效任务不执行,非常简单,参考如下实现(伪代码):

for(const job of queue){
    if(job.active !== false){
        job()
    }
}

删除任务

组件 DOM 更新(instance.update),是深度更新,会递归的对所有子组件执行 instance.update

因此,在父组件深度更新完成之后,不需要再重复更新子组件,更新前,需要将组件的 Job 从队列中删除

下图是任务删除的示意图:

image-20220119205435400

在一个组件 DOM 更新时,会先把该组件的 Job,从队列中删除。因为即将更新该组件,就不需要再排队执行了。

要实现删除 Job,非常简单:

export function invalidateJob(job) {
  // 找到 job 的索引
  const i = queue.indexOf(job)
  // 删除 Job
  queue.splice(i, 1)
}

// 在 instance.udpate 中删除当前组件的 Job
const job = instance.update = function(){
    invalidateJob(job)

    // 组件 DOM 更新
}

删除和失效,都是不执行该 Job,它们有什么使用上的区别?

失效 删除
场景 组件卸载时,将 Job 设置为失效,Job 从队列中取出时,不再执行 组件更新时,删除该组件在队列中的 Job
能否再次 加入队列 不能,会被去重 可以再次加入队列
意义 被卸载的组件,无论它依赖的响应式变量如何更新,该组件都不会更新了 删除任务,是因为已经更新过了,不需要重复更新。
如果依赖的响应式变量再次被修改,仍然需要加入队列,等待更新

Job 递归

递归这个特性,是 vue 调度中比较复杂的情况。如果暂时理解不了的,可以先继续往下看,不必过于扣细节。

Job 递归,就是 Job 在更新组件 DOM 的过程中,依赖的响应式变量发生变化,又调用 queueJob 把自身的 Job 加入到队列中

为什么会需要递归?

先做个类比,应该就大概明白了:

你刚拖好地,你儿子就又把地板踩脏了,你只有重新再拖一遍。

如果你一直拖,儿子一直踩,就是无限递归了。。。这时候就应该把儿子打一顿。。。

在组件 DOM 更新(instance.update)的过程中,可能会导致自身依赖的响应式变量改变,从而调用 queueJob,将自身 Job 加入到队列。

由于响应式数据被改变(因为脏了),需要整个组件重新更新(所以需要重新拖地)

下图就是一个组件 DOM 更新过程中,导致响应式变量变化的例子:

image-20220121151108748

父组件刚更新完,子组件由于属性更新,立即触发 watch,emit 事件,修改了父组件的 loading 响应式变量,导致父组件需要重新更新。

(watch 一般情况下,是加入到 Pre 队列等待执行,但在组件 DOM 更新时,watch也是加入队列,但会立即执行并清空 Pre 队列,暂时先记住有这个小特性即可)

Job 的结构是怎样的?

Job 的数据结构如下:

export interface SchedulerJob extends Function {
  id?: number   // 用于对队列中的 job 进行排序,id 小的先执行
  active?: boolean
  computed?: boolean
  allowRecurse?: boolean   // 表示 effect 是否允许递归触发本身
  ownerInstance?: ComponentInternalInstance  // 仅仅用在开发环境,用于递归超出次数时,报错用的
}

job 本身是一个函数,并且带有有一些属性。

  • id,表示优先级,用于实现队列插队,id 小的先执行
  • active:表示 Job 是否有效,失效的 Job 不执行。如组件卸载会导致 Job 失效
  • allowRecurse:是否允许递归

其他属性,我们可以先不关注,因为跟调度机制的核心逻辑无关。

队列的结构是怎样的?

queue 队列的数据结构如下:

const queue: SchedulerJob[] = []

队列的执行:

// 按优先级排序
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) {
        // 执行 Job 函数,并带有 Vue 内部的错误处理,用于格式化错误信息,给用户更好的提示
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 清空 queue 队列
    flushIndex = 0
    queue.length = 0
  }

在之前的图示讲解中,为了更好的理解队列,会把 Job 的执行,画成取出队列并执行。

而在真正写代码中,队列的执行,是不会把 Job 从 queue 中取出的,而是遍历所有的 Job 并执行,在最后清空整个 queue。

加入队列

queueJob

下面是 queue 队列的 Job,加入队列的实现:

export function queueJob(job: SchedulerJob) {
  if (
    (!queue.length ||
      // 去重判断
      !queue.includes(
        job,
        // isFlushing 表示正在执行队列
        // flushIndex 当前正在执行的 Job 的 index
        // queue.includes 函数的第二个参数,是表示从该索引开始查找
        // 整个表达式意思:如果允许递归,则当前正在执行的 Job,不加入去重判断
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      ))
  ) {
    if (job.id == null) {
      // 没有 id 的加入到队列末尾
      queue.push(job)
    } else {
      // 在指定位置加入 job
      // findInsertionIndex 是使用二分查找,找出合适的插入位置
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()   // 作用会在后面说
  }
}

这里有几个特性:

  • 去重
  • 处理递归,如果允许递归,则正在运行的 job,不加入去重判断
  • 优先级实现,按 id 从小到大,在队列合适的位置插入 Job;如果没有 id,则放到最后

queueCb

Pre 队列和 Post 队列的实现也大致相同,只不过是没有优先级机制(Post 队列的优先级在执行时处理):

function queueCb(
  cb: SchedulerJobs,
  activeQueue: SchedulerJob[] | null,
  pendingQueue: SchedulerJob[],
  index: number
) {
  if (!isArray(cb)) {
    if (
      !activeQueue ||
      // 去重判断
      !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
    ) {
      pendingQueue.push(cb)
    }
  } else {
    // if cb is an array, it is a component lifecycle hook which can only be
    // triggered by a job, which is already deduped in the main queue, so
    // we can skip duplicate check here to improve perf
    // 翻译:如果 cb 是一个数组,它只能是在一个 job 内触发的组件生命周期 hook(而且这些 cb 已经去重过了,可以跳过去重判断)
    pendingQueue.push(...cb)
  }
  queueFlush()
}

export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

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

小结

总的来说,加入队列函数,核心逻辑就都是如下:


function queueJob(){
    queue.push(job)
    queueFlush()  // 作用会在后面说
}

在这个基础上,另外再加上一些去重判断、和优先级而已。

为什么组件异步队列 queue 跟 Pre 队列、Post 队列的入队方式还不一样呢?

因为一些细节上的处理不一致

  • queue 队列有优先级
  • 而 Pre 队列、Post 队列的入参,可能是数组

但其实我们也不需要过分关心这些细节,因为我们学习源码,其实是为了学习它的优良设计,我们把设计学到就好了,在现实的项目中,我们几乎不会遇到一模一样的场景,因此掌握整体设计,比抠细节更重要

那么 queueFlush 有什么作用呢?

queueFlush 的作用,就好像是你第一个到饭堂打饭,阿姨在旁边坐着,你得提醒阿姨该给你打饭了。

队列其实并不是一直都在执行的,当列队为空之后,就会停止等到又有新的 Job 进来的时候,队列才会开始执行

queueFlush 在这里的作用,就是告诉队列可以开始执行了。

我们来看看 queueFlush 的实现:

let isFlushing = false  // 标记队列是否正在执行
let isFlushPending = false // 标记队列是否等待执行

function queueFlush() {
  // 如果不是正在执行队列 / 等待执行队列
  if (!isFlushing && !isFlushPending) {
    // 用于标记为等待执行队列
    isFlushPending = true
    // 在下一个微任务执行队列
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

执行队列的方法,是 flushJob。

queueFlush 是队列执行时机的实现 —— flushJob 会在下一个微任务时执行

为什么执行时机为下一个微任务?为什么不能是 setTimeout(flushJob, 0)

我们目的,是延迟执行 queueJob,等所有组件数据都更新完,再执行组件 DOM 更新(instance.update)。

要达到这一目的:我们只需要等在下一个浏览器任务,执行 queueJob 即可

因为,响应式数据的更新,都在当前的浏览器任务中。当 queueJob 作为微任务执行时,就表明上一个任务一定已经完成了。

而在浏览器中,微任务比宏任务有更高的优先级,因此 queueJob 使用微任务。

浏览器事件循环示意图如下:

image-20220113205347386

每次循环,浏览器只会取一个宏任务执行,而微任务则是执行全部,在微任务执行 queueJob,能在最快时间执行队列,并且接下来浏览器就会执行渲染页面,更新UI。

否则,如果 queueJob 使用宏任务,极端情况下,可能会有多个宏任务在 queueJob 之前,而每次事件循环,只会取一个宏任务,则 queueJob 的执行时机会在非常的后,这对用户体验来说是有一定的伤害的

至此,我们已经把下图蓝色部分都解析完了:

image-20220120000045131

剩下的是红色部分,即函数 flushJob 部分的实现了:

队列的执行 flushJob

function flushJobs() {
  // 等待状态设置为 false 
  isFlushPending = false
  // 标记队列为正在执行状态
  isFlushing = true

  // 执行 Pre 队列
  flushPreFlushCbs()

  // 根据 job id 进行排序,从小到大
  queue.sort((a, b) => getId(a) - getId(b))

  // 用于检测是否是无限递归,最多 100 层递归,否则就报错,只会开发模式下检查
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 循环组件异步更新队列,执行 job
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // 仅在 active 时才调用 job
      if (job && job.active !== false) {

        // 检查无限递归
        if (__DEV__ && check(job)) {
          continue
        }
        // 调用 job,带有错误处理
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 收尾工作,重置这些用于标记的变量
    flushIndex = 0  // 将队列执行的 index 重置
    queue.length = 0 // 清空队列

    // 执行 Post 队列
    flushPostFlushCbs()

    isFlushing = false
    currentFlushPromise = null

    // 如果还有 Job,继续执行队列
    // Post 队列运行过程中,可能又会将 Job 加入进来,会在下一轮 flushJob 执行
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs()
    }
  }
}

flushJob 主要执行以下内容:

  1. 执行 Pre 队列
  2. 执行queue 队列
  3. 执行 Post 队列
  4. 循环重新执行所有队列,直到所有队列都为空

执行 queue 队列

queue 队列执行对应的是这一部分:

try {
    // 循环组件异步更新队列,执行 job
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // 仅在 active 时才调用 job
      if (job && job.active !== false) {

        // 检查无限递归
        if (__DEV__ && check(job)) {
          continue
        }
        // 调用 job,带有错误处理
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 收尾工作,重置这些用于标记的变量
    flushIndex = 0  // 将队列执行的 index 重置
    queue.length = 0 // 清空队列
  }
}

循环遍历 queue,运行 Job,直到 queue 为空

queue 队列执行期间,可能会有新的 Job 入队,同样会被执行。

image-20220121163647496

执行 Pre 队列

export function flushPreFlushCbs() {
  // 有 Job 才执行
  if (pendingPreFlushCbs.length) {
    // 执行前去重,并赋值到 activePreFlushCbs
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    // pendingPreFlushCbs 清空
    pendingPreFlushCbs.length = 0

    // 循环执行 Job
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      // 开发模式下,校验无限递归的情况
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      // 执行 Job
      activePreFlushCbs[preFlushIndex]()
    }
    // 收尾工作
    activePreFlushCbs = null
    preFlushIndex = 0

    // 可能递归,再次执行 flushPreFlushCbs,如果队列为空就停止
    flushPreFlushCbs()
  }
}

主要流程如下:

  1. Job 最开始是在 pending 队列中的
  2. flushPreFlushCbs 执行时,将 pending 队列中的 Job 去重,并改为 active 队列
  3. 循环执行 active 队列的 Job
  4. 重复 flushPreFlushCbs,直到队列为空

image-20220121163922530

执行 Post 队列

export function flushPostFlushCbs(seen?: CountMap) {
  // 队列为空则结束
  if (pendingPostFlushCbs.length) {
    // 去重
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0

    // #1947 already has active queue, nested flushPostFlushCbs call
    // 特殊情况,发生了递归,在执行前 activePostFlushCbs 可能已经有值了,该情况可不必过多关注
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }

    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()
    }

    // 优先级排序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

    // 循环执行 Job
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // 在开发模式下,检查递归次数,最多 100 次递归
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {
        continue
      }
      // 执行 Job
      activePostFlushCbs[postFlushIndex]()
    }
    // 收尾工作
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

主要流程如下:

  1. Job 最开始是在 pending 队列中的
  2. flushPostFlushCbs 执行时,将 pending 队列中的 Job 去重,然后跟 active 队列合并
  3. 循环执行 active 队列的 Job

image-20220120213224077

为什么在队列最后没有像 Pre 队列那样,再次执行 flushPostFlushCbs?

Post 队列的 Job 执行时,可能会将 Job 继续加入到队列(Pre 队列,组件异步更新队列,Post 队列都可能)

新加入的 Job,会在下一轮 flushJob 中执行:

// postFlushCb 可能又会将 Job 加入进来,如果还有 Job,继续执行
if (
  queue.length ||
  pendingPreFlushCbs.length ||
  pendingPostFlushCbs.length
) {
  // 执行下一轮队列任务
  flushJobs()
}

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/vdUat0dAiR07t-5kJ-s6jg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 目录