preface

What is scheduling?

The concept of scheduling originally came from the operating system.

Due to the limitation of computer resources, we must choose tasks to occupy resources according to certain principles.

Operating systems introduce scheduling to solve the problem of allocating computer resources, because tasks are endless, but the CPU cannot perform all of them at the same time. For example, some high-priority tasks (such as user interactions that require immediate feedback) need to occupy resources/run first, which is a prioritized scheduling.

What is the scheduling of Vue? What’s the difference?

The scheduling of Vue also selects tasks to occupy resources/execute according to certain principles. But the same action, the purpose is not the same.

Because, Vue doesn’t have to solve the problem of computer resource allocation (which the operating system does). Vue uses scheduling algorithms to ensure that the Vue component rendering process is correct and the API execution sequence is correct.

In Vue3 API design, there are various asynchronous callback API design, such as: component life cycle, watch API callback function and so on. These callbacks are not executed immediately, but as jobs that need to be executed in a certain order

Here are some of the rules:

  • The watch callback function needs to be called before the component is updated
  • Component DOM updates need to be made after reactive data (changes to ref, Reactive, data, etc.) on which the Vue template depends is updated
  • The parent component needs to be updated first and the child component later
  • Mounted Life cycle. The life cycle must be executed after components are Mounted
  • Updated life cycle that needs to be executed after component updates

Vue API design has made this rule, when should perform what task, and this rule in the code implementation, is the scheduling algorithm.

Learn the purpose of Vue scheduling

Vue is not the inventor of scheduling algorithms, but the user and beneficiary of scheduling algorithms. These designs are based on ancestors’ exploration and precipitation, and then combined with their own needs to transform out.

The speed of front-end technology update iteration is very fast, but these excellent design, is the same, this is the purpose of our learning these excellent design, can do, with the same change.

Basic introduction to scheduling algorithms

Scheduling algorithms have two basic data structures: queue and Job.

  • Queue: Adds a task to a queue and waits for execution
  • Dequeue: The task is removed from the queue and executed immediately

There are many scheduling algorithms, all of which serve different purposes, but their basic data structures are the same, differing in the way they are queued and queued

The following are two common scheduling algorithms

  • First come, first served (FCFS) : Jobs that are added to the queue are executed first. This algorithm is commonly used in scenarios where jobs are equal and have no priority.
  • Priority scheduling algorithm: Jobs with a higher priority are executed first.

There is nothing about Vue in the scheduling algorithm, how to relate to Vue?

The scheduling algorithm is an abstraction of the entire scheduling process. The algorithm does not care about the content of the Job. As an infrastructure of Vue3, it plays a role of decoupling (if you cannot understand this for the moment, it will be explained in the next section).

The scheduling algorithm schedules only the order of execution, not the execution

So how does Vue use scheduling algorithms to achieve the correct scheduling of its OWN API? We will describe this in more detail later in the article

Use of Vue3 scheduling algorithm

Vue3’s scheduling algorithm is basically the same as the algorithm mentioned above, with some details adapted to Vue

Vue has 3 queues, which are:

  • A queue before component DOM updates (not component data updates) is also called a Pre queue
  • Component DOM update (not component data update) queue, also known as queue queue/component asynchronous update queue
  • The queue after component DOM updates (not component data updates) is also called the Post queue

Some features of the three queues are compared (just take a look, more details will be introduced later) :

The Pre queue The queue queue Post queue
Queue role Perform the task before the COMPONENT DOM update Perform component DOM updates Perform tasks after component DOM updates
A team approach First in first out Queue cutting is allowed, from smallest to largest by ID Queue cutting is allowed, from smallest to largest by ID

In the whole scheduling process, only the queuing process is controlled by ourselves, and the execution of the queue is controlled by the queue itself. Right

Therefore, only the queue API is exposed by the scheduling algorithm:

  • QueuePreFlushCb: Joins the Pre queue
  • QueueJob: Joins the queue
  • QueuePostFlushCb: Joins the Post queue

Here’s how:

const job1 = () = > {
    // Assume that this is the parent component's DOM update logic
    console.log('Parent component DOM update job 1')
}
job1.id = 1		Vue specifies that the smaller the ID, the higher the priority

const job2 = () = > {
    // Assume that this is the DOM update logic for the child component
    console.log('Child component DOM update job 2')
}
job2.id = 2		// Set the priority

// Join the queue
// Job 2 is added first but executed after job 1 because the id with a small id has a higher priority
queueJob(job2)
queueJob(job1)

// Join the Post queue
queuePostFlushCb(() = > {
    // Assume this is the updated life cycle
    console.log('Execute updated lifecycle 1')})// Join the Post queue
queuePostFlushCb(() = > {
    // Assume this is the updated life cycle
    console.log('Execute updated lifecycle 2')})// Join the Pre queue
queuePreFlushCb(() = > {
    // Suppose this is the watch callback
    console.log('Execute the watch callback function 1')})// Join the Pre queue
queuePreFlushCb(() = > {
    // Suppose this is the watch callback
    console.log('Execute the watch callback 2')})console.log('All responsive data updated')
Copy the code

The print result is as follows:

// All reactive data has been updated
// Execute the watch callback function 1
// Execute the watch callback 2
// The parent component DOM updates job 1
// The child DOM updates job 2
// Execute the updated lifecycle 1
// Execute the updated lifecycle 2
Copy the code

Queues are very simple to use. Just pass in the job function to the corresponding queue. The Pre queue, Queue, and Post queue will be executed after all the JS code of the current browser task is completed

Scheduling algorithm is an abstraction of the whole scheduling process

A queue selects a Job to execute according to its own nature (fifO or priority). A queue does not care what the Job content is.

This design can greatly reduce the coupling between the Vue API and the queue. The queue does not know the existence of the Vue API. Even if the Vue will add new asynchronous callback API in the future, there is no need to modify the queue.

In the example above, you can see how Vue3 uses the scheduling API to control the timing of various types of asynchronous callbacks. For different asynchronous callback apis, different queues are used depending on when the API is designed to execute.

Such as:

  • The watch callback function, by default, is executed before component DOM updates, so Pre queues are used.
  • Component DOM updates using queue.
  • The updated life cycle needs to be executed after component DOM updates, so the Post queue is used.

This article will not introduce too much about the implementation of the specific content of the Job (different apis, the content of the Job are different), but focus on the internal implementation of the scheduling mechanism, next we will look into the internal scheduling mechanism of Vue.

Term contract

From an example, we understand the various nouns used:

<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 relies on count and touches queueJob(instance.update)
}
</script>
Copy the code

Reactive data update

It refers to the change of reactive data such as REF, Reactive, and component data that the template depends on

The reactive data count. Value is modified in the click callback triggered by a button click

Component DOM update

The instance.update function is actually called, which compares the VNode before the data component is updated with the VNode after the data component is updated, compares the difference, and modifies the DOM of the difference part. This process is called patch, and the method used to compare VNodes is called the diff algorithm (since there is no space to do this here, so just keep in mind the features of instance.update).

  • Instance refers to the internal component instance of Vue, which is not accessible by direct use.

  • Instance. update is a deep update, that is, in addition to updating the component itself, instance.update of the child component is recursively called, so the process updates the entire component tree.

  • Instance. update updates the component’s properties (if the parent component’s incoming changes) and then updates its corresponding DOM

  • ** Reactive update ≠ component DOM ** update, reactive update only changes variable value, DOM has not been modified, but queueJob(instance.update) is executed immediately to queue component DOM update task. That is, data changes take effect immediately, but DOM changes are delayed

Scheduling details

Use a table to summarize some of the details of the three scheduling processes

The Pre queue The queue queue Post queue
Queue role Perform the task before the COMPONENT DOM update Perform component DOM updates Perform tasks after component DOM updates
Task to heavy duplicate removal duplicate removal duplicate removal
A team approach First in first out Queue cutting is allowed, from smallest to largest by ID Queue cutting is allowed, from smallest to largest by ID
Task validity All missions are valid. When a component is uninstalled, the corresponding task is invalid All missions are valid.
Delete the task Don’t need Some tasks need to be deleted Don’t need
The Job of recursion The default allows The default allows The default allows

Next, let’s analyze each detail:

Task to heavy

Each time a reactive variable is modified (that is, the corresponding reactive data is modified), the component DOM update Job is queued.

QueueJob is called immediately when a component dependent reactive variable is modified
queueJob(instance.update)
Copy the code

So when we modify a reactive variable that the same component depends on multiple times, we call queueJob multiple times.

Here’s a simple example:

<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 relies on count and touches queueJob(instance.update)
  count.value = count.value + 2		// Template relies on count and touches queueJob(instance.update)
}
</script>
Copy the code

If count. Value is changed twice, queueJobs are triggered twice.

To prevent multiple iterations of the update, we need to de-duplicate the Job when enqueueing (pseudocode) :

export function queueJob(job: SchedulerJob) {
  // rejudge
  if(! queue.includes(job)) {/ / team
    queue.push(job)
  }
}
Copy the code

The enqueue functions of other queues have similar de-duplication logic.

Priority mechanism

Only queue and Post queues have a priority mechanism. The smaller the job.id is, the earlier the queue is executed.

Why do I need a priority queue?

Queue queues and Post queues use priorities for different reasons.

Let’s break it down one by one:

Queue Indicates the priority mechanism of queues

The Job of the queue is to perform DOM updates of the component. In Vue, components are not all independent of each other; they previously had a parent-child relationship

The parent must be updated before the child can be updated, because the parent may pass parameters to the child (as properties of the child)

The following figure shows the order in which the parent and child components and their properties are updated:

Before the parent component DOM is updated, the props of the child component is modified. Therefore, the parent component DOM update must be performed first. The props of the child component is the correct value.

Therefore: Parent component priority > child component priority.

How do I ensure that the parent component has a higher priority? How to ensure that the Job. Id of the parent component is smaller?

As we said in the previous section, component DOM updates deeply recursively update child components. The same goes for component creation, which also creates child components deeply recursively.

Here is a schematic of a component tree, created in the following order:

Depth-created components are created in the order in which they are traversed through the depth of the tree. Depth traversal must be traversal of the parent node, then traversal of the child node

Therefore, it can be seen from the figure that the serial number of the parent component must be smaller than that of the child component. Using the serial number as job. id ensures that the parent component has a higher priority than the child component

Here we can get a sense of how deep traversal can be used to deal with the order of dependencies.

We learn the source code, we learn the algorithm, we learn the design.

Deep traversal will come to mind in future projects when we have to rely on who executes first.

To implement queue Job priority, we only need to implement queue jumping:

export function queueJob(job: SchedulerJob) {
  // rejudge
  if ( !queue.includes(job) ) {
    // No id is put last
    if (job.id == null) {
      queue.push(job)
    } else {
      // find job. Id, calculate insert position
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
  }
}
Copy the code

Priority mechanism for Post queues

Post queue jobs: Post queue jobs

  • Mounted, updated, etc. They have a common feature, that is, they need to wait for DOM update before executing
  • WatchPostEffect API, the user manually sets the watch callback to execute after the DOM update

There is no dependency between these user-defined callbacks

So why do Post queues need a priority?

Because there’s an internal Job that needs to be done ahead of time, which is to update the template reference.

Because a template reference may be used in a user-written callback function, the value of the template reference must be updated before the user-written callback function is executed.

Look at the following code:

<template>
  <button @click='add' >count: {{ count }}</button>
  <div v-if="count % 2" :ref="divRef">The count is even</div>
  <div v-else :ref="divRef">The count is an odd number of</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>
Copy the code

Divref. value points to different DOM nodes when count is odd or even.

DivRef must be updated before the user-written updated life cycle is executed, or the wrong value will be fetched.

Therefore, updating the Job referenced by the template, job.id = -1, will be executed first

Jobs set by other users without job.id are added to the end of the queue and executed at the end.

Failure of the task

When a component is unmounted, its Job becomes invalid because the component does not need to be updated. Failed tasks are not executed when they are removed from the queue.

Only jobs in the queue fail.

Here is a schematic of a failure case:

  1. Click the button and count. Value changes
  2. The count variable changes in response, and queueJob queues the child Job immediately
  3. Emit event, parent component hasChild.value changed
  4. QueueJob queues the parent Job immediately after a hasChild variable changes in response
  5. The parent component has higher priority and is executed first.
  6. Update the parent component DOM. The child component is unloaded due to v-if
  7. When a child component is uninstalled, its Job is invalidated.Job.active = false

To implement the invalidation task not executing, it is very simple, see the following implementation (pseudocode) :

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

Delete the task

Component DOM updates (instance.update), which are deep updates, recursively execute instance.update on all child components.

Therefore, after the in-depth update of the parent component is complete, the child component does not need to be updated repeatedly. Before the update, the Job of the component needs to be deleted from the queue

Below is a schematic of task deletion:

When a component’s DOM is updated, the component’s Job is first removed from the queue. Because the component is about to be updated, there is no need to queue execution.

To delete a Job, it is very simple:

export function invalidateJob(job) {
  // Find the index of job
  const i = queue.indexOf(job)
  / / remove the Job
  queue.splice(i, 1)}// Delete the current component's Job in instance.udpate
const job = instance.update = function(){
    invalidateJob(job)
    
    // Component DOM updates
}
Copy the code

Deletion and invalidation do not execute the Job. What is the difference between them?

failure delete
scenario When a component is uninstalled, the Job is invalid. When the Job is removed from the queue, the Job will not be executed When a component is updated, the Job of the component in the queue is deleted
Can you again

Join the queue
No, it will be removed You can join the queue again
meaning The unloaded component will not be updated no matter how the reactive variable it depends on is updated Delete a task because it has been updated and does not need to be updated again.

If the dependent reactive variable is changed again, it still needs to be queued for updates

The Job of recursion

Recursion is one of the more complex aspects of VUE scheduling. If you don’t understand temporarily, you can continue to read, don’t buckle the details.

In Job recursion, when a Job updates the DOM of a component, the dependent responsive variable changes, and queueJob is called to add its Job to the queue.

Why do we need recursion?

Let’s make an analogy to get the idea:

No sooner had you finished cleaning the floor than your son tramped it all over again and you had to do it all over again.

If you keep dragging and your son keeps stepping, it’s an infinite recursion… This is the time to beat your son…

During component DOM update (instance.update), dependent reactive variables may change, and queueJob may be called to queue its Job.

Because the reactive data is changed (because it is dirty), the entire component needs to be updated (so it needs to be mopped again)

The following is an example of a responsive variable change during component DOM updates:

As the parent component was just updated, the child component immediately triggered the watch and emit event due to the property update, and modified the loading responsive variable of the parent component, resulting in the need to update the parent component again.

(In general, watch joins the Pre queue and waits for execution. However, when component DOM is updated, Watch also joins the queue and immediately executes and empties the Pre queue. Keep this feature in mind for now.)

What’s the structure of a Job?

The data structure of a Job is as follows:

export interface SchedulerJob extendsFunction { id? :number			// Sort the jobs in the queue. The jobs with small ids are executed firstactive? :booleancomputed? :booleanallowRecurse? :boolean   // Indicates whether effect allows recursive firing itselfownerInstance? : ComponentInternalInstance// This is only used in the development environment
}
Copy the code

Job itself is a function with some attributes.

  • Id: indicates the priority. It is used to implement queue jumping. The smaller ID is executed first
  • Active: indicates whether the Job is valid. Invalid jobs are not executed. If the component is uninstalled, the Job fails
  • AllowRecurse: Whether recursion is allowed

The other attributes, we can ignore for now, are irrelevant to the core logic of the scheduling mechanism.

What is the structure of the queue?

Queue Has the following data structure:

const queue: SchedulerJob[] = []
Copy the code

Queue execution:

// Sort by priority
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) {
        // Execute the Job function with Vue internal error handling to format error messages and give the user a better hint
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // Clear the queue
    flushIndex = 0
    queue.length = 0
  }
Copy the code

In the previous illustration, to better understand the queue, we drew the execution of the Job as fetching the queue and executing it.

In real code, the queue execution, instead of pulling the Job out of the queue, traverses all the jobs and executes them, emptying the queue at the end.

Join the queue

queueJob

Queue jobs queue jobs queue jobs queue jobs queue jobs

export function queueJob(job: SchedulerJob) {
  if (
    (!queue.length ||
      // rejudge! queue.includes( job,// isFlushing means that the queue is being executed
        // flushIndex Specifies the index of the Job that is being executed
        // Queue.includes takes the second argument to start the search from the index
        // If recursion is allowed, jobs that are currently being executed will not be re-judged
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      ))
  ) {
    if (job.id == null) {
      // Join the queue with no ID
      queue.push(job)
    } else {
      // Add the job at the specified position
      // findInsertionIndex uses binary search to find the appropriate insertion position
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()   // Function will say later}}Copy the code

There are several features:

  • duplicate removal
  • Handle recursion. If recursion is allowed, the running job does not participate in the rescheduling judgment
  • Priority implementation, according to the ID from small to large, the appropriate position in the queue to insert jobs; If there is no ID, put it last

queueCb

Pre and Post queues are also implemented in much the same way, except that there is no priority mechanism (the priority of the Post queue is handled at execution time) :

function queueCb(
  cb: SchedulerJobs,
  activeQueue: SchedulerJob[] | null,
  pendingQueue: SchedulerJob[],
  index: number
) {
  if(! isArray(cb)) {if (
      !activeQueue ||
      // rejudge! 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
    If cb is an array, it can only be a component lifecycle hook that is triggered within a job.pendingQueue.push(... cb) } queueFlush() }export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

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

summary

In general, the core logic of adding a queue function is as follows:

function queueJob(){
    queue.push(job)
    queueFlush()  // Function will say later
}
Copy the code

On top of that, add some de-judging and prioritization.

Why is a component asynchronous queue not enqueued in the same way as a Pre or Post queue?

Because some of the details are not consistent

  • Queue Indicates the priority of the queue
  • The input arguments to the Pre queue, the input arguments to the Post queue, might be arrays

But in fact, we do not need to care too much about these details, because we learn the source code, in fact, is to learn its excellent design, we learn the design, in the real project, we will almost not encounter the same scene, so master the overall design, more important than the details

So what does queueFlush do?

QueueFlush is like when you’re the first person in the cafeteria to get your meal and your aunt is sitting next to you and you have to remind her that it’s time to get your meal.

The queue is not executed all the time. When the queue is empty, the queue stops. When a new Job enters the queue, the queue starts to execute

The purpose of queueFlush here is to tell the queue that it is ready to execute.

Let’s look at the implementation of queueFlush:

let isFlushing = false  // Flags whether the queue is executing
let isFlushPending = false // flags whether the queue is waiting to execute

function queueFlush() {
  // If not executing queue/waiting for executing queue
  if(! isFlushing && ! isFlushPending) {// Used to mark the queue for execution
    isFlushPending = true
    // Next microtask execution queue
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
Copy the code

The method to execute a queue is flushJob.

QueueFlush is an implementation of queue execution timing — flushJob is executed at the next microtask.

Why execute the next microtask? SetTimeout (flushJob, 0)

The purpose is to delay queueJob until all component data is updated before component DOM update (instance.update).

To do this, we simply wait for the next browser task to execute queueJob

Because updates to responsive data are in the current browser task. When queueJob is executed as a microtask, it indicates that the previous task must have completed.

In browsers, microtasks have a higher priority than macro tasks, so queueJob uses microtasks.

The browser event loop looks like this:

In each loop, the browser only takes one macro task to execute, while the microtask performs all tasks. In the microtask, the browser performs queueJob, which executes the queue as soon as possible, and then the browser performs rendering of the page and updating the UI.

Otherwise, if queueJob uses macro tasks, in extreme cases, multiple macro tasks may precede queueJob, and only one macro task may be retrieved in each event loop. As a result, queueJob execution may be very late, which may cause harm to user experience

So far, we have parsed all the blue parts of the figure below:

The rest is the red part, which implements the flushJob part:

The execution of the queue is flushJob

function flushJobs() {
  // Set the wait state to false
  isFlushPending = false
  // marks the queue as executing
  isFlushing = true

  // Execute Pre queue
  flushPreFlushCbs()

  // Sort by job ID, from smallest to largest
  queue.sort((a, b) = > getId(a) - getId(b))

  // Check for infinite recursion, up to 100 recursions, otherwise an error will be reported, only in development mode check
  const check = __DEV__
    ? (job: SchedulerJob) = >checkRecursiveUpdates(seen! , job) : NOOPtry {
    // The loop component asynchronously updates the queue and executes the job
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // Job is called only when active
      if(job && job.active ! = =false) {
          
        // Check for infinite recursion
        if (__DEV__ && check(job)) {
          continue
        }
        // Call job with error handling
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // Close up by resetting the variables used for the tag
    flushIndex = 0		// Resets the index of queue execution
    queue.length = 0	// Clear the queue

    // Execute the Post queue
    flushPostFlushCbs()

    isFlushing = false
    currentFlushPromise = null
     
    // If there are still jobs, continue the queue execution
    // When the Post queue is running, the Job may be added again and executed in the next flushJob
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs()
    }
  }
}
Copy the code

FlushJob executes the following:

  1. Executing the Pre queue
  2. Executing a queue
  3. Executing the Post queue
  4. The loop reexecutes all queues until all queues are empty

Executing a queue

Queue execution corresponds to this section:

try {
    // The loop component asynchronously updates the queue and executes the job
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      // Job is called only when active
      if(job && job.active ! = =false) {
          
        // Check for infinite recursion
        if (__DEV__ && check(job)) {
          continue
        }
        // Call job with error handling
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // Close up by resetting the variables used for the tag
    flushIndex = 0		// Resets the index of queue execution
    queue.length = 0	// Clear the queue}}Copy the code

Loop through the queue, running the Job until the queue is empty

Queue During queue execution, new jobs may be added and executed.

Executing the Pre queue

export function flushPreFlushCbs() {
  // Execute only when there is a Job
  if (pendingPreFlushCbs.length) {
    // Perform the pre-flush and assign the value to activePreFlushCbs
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    / / pendingPreFlushCbs empty
    pendingPreFlushCbs.length = 0

    // Execute the Job loop
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      // In development mode, check infinite recursion
      if( __DEV__ && checkRecursiveUpdates(seen! , activePreFlushCbs[preFlushIndex]) ) {continue
      }
      / / perform the Job
      activePreFlushCbs[preFlushIndex]()
    }
    // Wrap things up
    activePreFlushCbs = null
    preFlushIndex = 0
      
    FlushPreFlushCbs again, stopping if the queue is empty
    flushPreFlushCbs()
  }
}
Copy the code

The main process is as follows:

  1. The Job starts out in the pending queue
  2. FlushPreFlushCbs When the flushCBS is executed, the jobs in the pending queue are deduplicated and changed to the active queue
  3. Execute the active queue Job in a loop
  4. Repeat flushPreFlushCbs until the queue is empty

Executing the Post queue

export function flushPostFlushCbs(seen? : CountMap) {
  // If the queue is empty, it ends
  if (pendingPostFlushCbs.length) {
    / / to heavy
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0

    // #1947 already has active queue, nested flushPostFlushCbs call
    ActivePostFlushCbs may already have a value before execution. This case is not worth paying attention to
    if(activePostFlushCbs) { activePostFlushCbs.push(... deduped)return
    }

    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()}// Sort by priority
    activePostFlushCbs.sort((a, b) = > getId(a) - getId(b))

    // Execute the Job loop
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      // In development mode, check recursion times, up to 100 recursions
      if( __DEV__ && checkRecursiveUpdates(seen! , activePostFlushCbs[postFlushIndex]) ) {continue
      }
      / / perform the Job
      activePostFlushCbs[postFlushIndex]()
    }
    // Wrap things up
    activePostFlushCbs = null
    postFlushIndex = 0}}Copy the code

The main process is as follows:

  1. The Job starts out in the pending queue
  2. FlushPostFlushCbs In flushCBS, jobs in the pending queue are deduplicated and merged with the active queue
  3. Execute the active queue Job in a loop

Why isn’t flushPostFlushCbs executed again at the end of the queue, as in the Pre queue?

When a Job in a Post queue is executed, the Job may be added to the queue (Pre queue, component asynchronous update queue, or Post queue).

A new Job is executed in the next flushJob:

// postFlushCb may add a Job again, if there is still a Job, continue execution
if (
  queue.length ||
  pendingPreFlushCbs.length ||
  pendingPostFlushCbs.length
) {
  // Execute the next queue task
  flushJobs()
}
Copy the code

The last

I wrote two articles about VUE queues before, but I always felt that I could not express the desired meaning well.

  • Different narrative thinking | take you understand vue3 internal queue step by step
  • Understand vuE3’s queue model in principle

It happened that the company was going to conduct promotion defense recently. After listening to colleagues’ pre-defense, I became more and more aware that personal expression ability is as important as technical ability. How to express one thing clearly (let others know what you have done in a limited time) is also a very important ability.

Therefore, I decided to revise again on the basis of these two articles. The whole process, including the preparation of the first two articles, took one and a half months to write before and after, and added a lot of pictures and details, hoping to help you better understand.

If this article is of any help to you, please give me a thumbs-up 👍. Your encouragement is the biggest power on my way of creation.