The role of nextTick

The official description is

Defer the callback until after the next DOM update cycle. Use it immediately after you have changed some data to wait for DOM updates.

It takes some time to update the DOM after data changes in the VUE. However, if we operate or acquire the DOM immediately after data update, it is still the unupdated DOM, and we can call nextTick to get the latest DOM

import { createApp, nextTick } from "vue"

const app = createApp({
  setup() {
    const message = ref("Hello!")
    const changeMessage = async newMessage => {
      message.value = newMessage
      await nextTick()
      console.log("Now DOM is updated")}}})Copy the code

Unit testing

Let’s read a quick look at the source code

NextTick unit tests directory location: Packages/Runtime-core /__tests__/scheduler.spec.ts

nextTick

it("nextTick".async() = > {const calls: string[] = []
  const dummyThen = Promise.resolve().then()
  const job1 = () = > {
    calls.push("job1")}const job2 = () = > {
    calls.push("job2")
  }
  nextTick(job1)
  job2()
  expect(calls.length).toBe(1)
  await dummyThen
  // job1 will be pushed in nextTick
  expect(calls.length).toBe(2)
  expect(calls).toMatchObject(["job2"."job1"])})Copy the code

NextTick receives a function as a parameter and adds it to the microtask queue. When the macro task is finished, the function in the microtask queue is executed, and job1 is executed

queueJob

Basic usage

it("basic usage".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const job2 = () = > {
    calls.push("job2")
  }
  queueJob(job1)
  queueJob(job2)
  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["job1"."job2"])})Copy the code

QueueJob, which receives a function as an argument and stores the functions in a queue in order, is a microtask

When refreshing jobs, insert jobs in ascending order of their IDS

it("should insert jobs in ascending order of job's id when flushing".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")

    queueJob(job2)
    queueJob(job3)
  }

  const job2 = () = > {
    calls.push("job2")
    queueJob(job4)
    queueJob(job5)
  }
  job2.id = 10

  const job3 = () = > {
    calls.push("job3")
  }
  job3.id = 1

  const job4 = () = > {
    calls.push("job4")}const job5 = () = > {
    calls.push("job5")
  }

  queueJob(job1)

  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["job1"."job3"."job2"."job4"."job5"])})Copy the code

If you have any id attribute queueJob receiving function, can add queue according to the id in ascending, not specified to join to the end

QueueJob resets jobs in queues

it("should dedupe queued jobs".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const job2 = () = > {
    calls.push("job2")
  }
  queueJob(job1)
  queueJob(job2)
  queueJob(job1)
  queueJob(job2)
  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["job1"."job2"])})Copy the code

QueueJob deletes duplicate jobs that are added to the queue to ensure the same sequence

QueueJob at flush time

    it('queueJob while flushing'.async() = > {const calls: string[] = []
      const job1 = () = > {
        calls.push('job1')
        // job2 will be executed after job1 at the same tick
        queueJob(job2)
      }
      const job2 = () = > {
        calls.push('job2')
      }
      queueJob(job1)

      await nextTick()
      expect(calls).toEqual(['job1'.'job2'])})})Copy the code

If queueJob(joB2) is called inside Job1, joB2 will be executed at the same time after JoB1, not waiting for the next microtask

queuePreFlushCb

Basic usage

it("basic usage".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")}const cb2 = () = > {
    calls.push("cb2")
  }

  queuePreFlushCb(cb1)
  queuePreFlushCb(cb2)

  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"])})Copy the code

QueuePreFlushCb is similar to queueJob in that it takes a function as an argument and is queued sequentially for execution in the microtask queue

PreFlushCb reflushCB in the queue

it("should dedupe queued preFlushCb".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")}const cb2 = () = > {
    calls.push("cb2")}const cb3 = () = > {
    calls.push("cb3")
  }

  queuePreFlushCb(cb1)
  queuePreFlushCb(cb2)
  queuePreFlushCb(cb1)
  queuePreFlushCb(cb2)
  queuePreFlushCb(cb3)

  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"."cb3"])})Copy the code

PreFlushCb will also discard loads

Chain queuePreFlushCb

it("chained queuePreFlushCb".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")
    // cb2 will be executed after cb1 at the same tick
    queuePreFlushCb(cb2)
  }
  const cb2 = () = > {
    calls.push("cb2")
  }
  queuePreFlushCb(cb1)

  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"])})Copy the code

If queuePreFlushCb(joB2) is called inside CB1, cb2 will execute at the same time after CB1, without waiting for the next microtask

queueJob with queuePreFlushCb

The queueJob preFlushCb

it("queueJob inside preFlushCb".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const cb1 = () = > {
    // queueJob in postFlushCb
    calls.push("cb1")
    queueJob(job1)
  }

  queuePreFlushCb(cb1)
  await nextTick()
  expect(calls).toEqual(["cb1"."job1"])})Copy the code

In preFlushCb, jobs can be nested and executed immediately

QueueJob and preFlushCb in preFlushCb

it("queueJob & preFlushCb inside preFlushCb".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const cb1 = () = > {
    calls.push("cb1")
    queueJob(job1)
    // cb2 should execute before the job
    queuePreFlushCb(cb2)
  }
  const cb2 = () = > {
    calls.push("cb2")
  }

  queuePreFlushCb(cb1)
  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"."job1"])})Copy the code

QueuePreFlushCb nested in preFlushCb is executed before queueJob nested in preFlushCb. That is, queuePreFlushCb has a higher priority than queueJob

The preFlushCb queueJob

it("preFlushCb inside queueJob".async() = > {const calls: string[] = []
  const job1 = () = > {
    queuePreFlushCb(cb1)
    queuePreFlushCb(cb2)
    flushPreFlushCbs(undefined, job1)
    calls.push("job1")}const cb1 = () = > {
    calls.push("cb1")
    // a cb triggers its parent job, which should be skipped
    queueJob(job1)
  }
  const cb2 = () = > {
    calls.push("cb2")}})Copy the code

QueuePreFlushCb can be nested in a job. If a parent job is called in a nested CB, the call is skipped

PreFlushCb in the postFlushCb queue

it("queue preFlushCb inside postFlushCb".async() = > {const cb = jest.fn()
  queuePostFlushCb(() = > {
    queuePreFlushCb(cb)
  })
  await nextTick()
  expect(cb).toHaveBeenCalled()
})
Copy the code

The postFlushCb can be nested with queuePreFlushCb. QueuePreFlushCb is executed immediately

queuePostFlushCb

Basic usage

describe("queuePostFlushCb".() = > {
  it("basic usage".async() = > {const calls: string[] = []
    const cb1 = () = > {
      calls.push("cb1")}const cb2 = () = > {
      calls.push("cb2")}const cb3 = () = > {
      calls.push("cb3")
    }

    queuePostFlushCb([cb1, cb2])
    queuePostFlushCb(cb3)

    expect(calls).toEqual([])
    await nextTick()
    expect(calls).toEqual(["cb1"."cb2"."cb3"])})})Copy the code

QueuePostFlushCb can take a function or an array of functions as arguments, queue them sequentially, and execute them in a microtask queue

QueuePostFlushCb reflushCB in the queue

it("should dedupe queued postFlushCb".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")}const cb2 = () = > {
    calls.push("cb2")}const cb3 = () = > {
    calls.push("cb3")
  }

  queuePostFlushCb([cb1, cb2])
  queuePostFlushCb(cb3)

  queuePostFlushCb([cb1, cb3])
  queuePostFlushCb(cb2)

  expect(calls).toEqual([])
  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"."cb3"])})Copy the code

QueuePostFlushCb rewrites functions in the queue, even functions that are passed through an array

Refresh queuePostFlushCb

it("queuePostFlushCb while flushing".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")
    // cb2 will be executed after cb1 at the same tick
    queuePostFlushCb(cb2)
  }
  const cb2 = () = > {
    calls.push("cb2")
  }
  queuePostFlushCb(cb1)

  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"])})Copy the code

The nested queuePostFlushCb is executed immediately

queueJob with queuePostFlushCb

QueueJob postFlushCb

it("queueJob inside postFlushCb".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const cb1 = () = > {
    // queueJob in postFlushCb
    calls.push("cb1")
    queueJob(job1)
  }

  queuePostFlushCb(cb1)
  await nextTick()
  expect(calls).toEqual(["cb1"."job1"])})Copy the code

In the postFlushCb file, a queueJob can be nested, and the queueJob is executed immediately

PostFlushCb queueJob and postFlushCb in flushCB

it("queueJob & postFlushCb inside postFlushCb".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")}const cb1 = () = > {
    calls.push("cb1")
    queuePostFlushCb(cb2)
    // job1 will executed before cb2
    // Job has higher priority than postFlushCb
    queueJob(job1)
  }
  const cb2 = () = > {
    calls.push("cb2")
  }

  queuePostFlushCb(cb1)
  await nextTick()
  expect(calls).toEqual(["cb1"."job1"."cb2"])})Copy the code

QueueJob is executed before queuePostFlushCb, that is, queueJob has a higher priority than queuePostFlushCb

PostFlushCb queueJob

it("postFlushCb inside queueJob".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")
    // postFlushCb in queueJob
    queuePostFlushCb(cb1)
  }
  const cb1 = () = > {
    calls.push("cb1")
  }

  queueJob(job1)
  await nextTick()
  expect(calls).toEqual(["job1"."cb1"])})Copy the code

You can nest queuePostFlushCb in a job. QueuePostFlushCb is executed immediately

QueueJob and postFlushCb are in queueJob

it("queueJob & postFlushCb inside queueJob".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")
    // cb1 will executed after job2
    // Job has higher priority than postFlushCb
    queuePostFlushCb(cb1)
    queueJob(job2)
  }
  const job2 = () = > {
    calls.push("job2")}const cb1 = () = > {
    calls.push("cb1")
  }

  queueJob(job1)
  await nextTick()
  expect(calls).toEqual(["job1"."job2"."cb1"])})Copy the code

QueueJob is executed before queuePostFlushCb. QueueJob has a higher priority than queuePostFlushCb

Nested queueJob and postFlush

it("nested queueJob w/ postFlushCb".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")

    queuePostFlushCb(cb1)
    queueJob(job2)
  }
  const job2 = () = > {
    calls.push("job2")
    queuePostFlushCb(cb2)
  }
  const cb1 = () = > {
    calls.push("cb1")}const cb2 = () = > {
    calls.push("cb2")
  }

  queueJob(job1)
  await nextTick()
  expect(calls).toEqual(["job1"."job2"."cb1"."cb2"])})Copy the code

Call queueJob(job2) in joB1. QueuePostFlushCb in joB2 is executed after queuePostFlushCb at the same level as queueJob(job2)

Invalid operation

test("invalidateJob".async() = > {const calls: string[] = []
  const job1 = () = > {
    calls.push("job1")
    invalidateJob(job2)
    job2()
  }
  const job2 = () = > {
    calls.push("job2")}const job3 = () = > {
    calls.push("job3")}const job4 = () = > {
    calls.push("job4")}// queue all jobs
  queueJob(job1)
  queueJob(job2)
  queueJob(job3)
  queuePostFlushCb(job4)
  expect(calls).toEqual([])
  await nextTick()
  // job2 should be called only once
  expect(calls).toEqual(["job1"."job2"."job3"."job4"])})Copy the code

InvalidateJob Disables a job

Sort jobs by ID

test("sort job based on id".async() = > {const calls: string[] = []
  const job1 = () = > calls.push("job1")
  // job1 has no id
  const job2 = () = > calls.push("job2")
  job2.id = 2
  const job3 = () = > calls.push("job3")
  job3.id = 1

  queueJob(job1)
  queueJob(job2)
  queueJob(job3)
  await nextTick()
  expect(calls).toEqual(["job3"."job2"."job1"])})Copy the code

If you have any id attribute queueJob receiving function, can add queue according to the id in ascending, not specified to join to the end

SchedulerCbs are sorted by ID

test("sort SchedulerCbs based on id".async() = > {const calls: string[] = []
  const cb1 = () = > calls.push("cb1")
  // cb1 has no id
  const cb2 = () = > calls.push("cb2")
  cb2.id = 2
  const cb3 = () = > calls.push("cb3")
  cb3.id = 1

  queuePostFlushCb(cb1)
  queuePostFlushCb(cb2)
  queuePostFlushCb(cb3)
  await nextTick()
  expect(calls).toEqual(["cb3"."cb2"."cb1"])})Copy the code

If queuePostFlushCb receives a function that has an ID attribute, it will be queued in ascending order by ID. If this is not specified, it will be queued in Infinity by default

Avoid repeated postFlushCb calls

test("avoid duplicate postFlushCb invocation".async() = > {const calls: string[] = []
  const cb1 = () = > {
    calls.push("cb1")
    queuePostFlushCb(cb2)
  }
  const cb2 = () = > {
    calls.push("cb2")
  }
  queuePostFlushCb(cb1)
  queuePostFlushCb(cb2)
  await nextTick()
  expect(calls).toEqual(["cb1"."cb2"])})Copy the code

Repeated calls to postFlushCb are not executed

NextTick should catch scheduler refresh errors

test("nextTick should capture scheduler flush errors".async() = > {const err = new Error("test")
  queueJob(() = > {
    throw err
  })
  try {
    await nextTick()
  } catch (e) {
    expect(e).toBe(err)
  }
  expect(
    `Unhandled error during execution of scheduler flush`,
  ).toHaveBeenWarned()

  // this one should no longer error
  await nextTick()
})
Copy the code

NextTick catches errors

Self-triggering jobs should be prevented by default

test("should prevent self-triggering jobs by default".async() = > {let count = 0
  const job = () = > {
    if (count < 3) {
      count++
      queueJob(job)
    }
  }
  queueJob(job)
  await nextTick()
  // only runs once - a job cannot queue itself
  expect(count).toBe(1)})Copy the code

By default, a job cannot recursively call itself

Explicitly marked jobs should be allowed to trigger themselves

test("should allow explicitly marked jobs to trigger itself".async() = > {// normal job
  let count = 0
  const job = () = > {
    if (count < 3) {
      count++
      queueJob(job)
    }
  }
  job.allowRecurse = true
  queueJob(job)
  await nextTick()
  expect(count).toBe(3)

  // post cb
  const cb = () = > {
    if (count < 5) {
      count++
      queuePostFlushCb(cb)
    }
  }
  cb.allowRecurse = true
  queuePostFlushCb(cb)
  await nextTick()
  expect(count).toBe(5)})Copy the code

If the allowRecurse attribute of job or postFlushCb is set to true, they can recursively call themselves

Duplicate queues should be prevented

test("should prevent duplicate queue".async() = > {let count = 0
  const job = () = > {
    count++
  }
  job.cb = true
  queueJob(job)
  queueJob(job)
  await nextTick()
  expect(count).toBe(1)})Copy the code

Prevent job invocation repeatedly

flushPostFlushCbs

test("flushPostFlushCbs".async() = > {let count = 0

  const queueAndFlush = (hook: Function) = > {
    queuePostFlushCb(hook)
    flushPostFlushCbs()
  }

  queueAndFlush(() = > {
    queueAndFlush(() = > {
      count++
    })
  })

  await nextTick()
  expect(count).toBe(1)})Copy the code

FlushPostFlushCbs causes the recursion in queuePostFlushCb to be executed only once

Stopped Reactive Effects are not running

test("should not run stopped reactive effects".async() = > {const spy = jest.fn()

  // simulate parent component that toggles child
  const job1 = () = > {
    // @ts-ignore
    job2.active = false
  }
  // simulate child that's triggered by the same reactive change that
  // triggers its toggle
  const job2 = () = > spy()
  expect(spy).toHaveBeenCalledTimes(0)

  queueJob(job1)
  queueJob(job2)
  await nextTick()

  // should not be called
  expect(spy).toHaveBeenCalledTimes(0)})Copy the code

If the active attribute of the job is false, the job will not be executed

summary

  • queueJobReceiving a functionjobAs a parameter, ifjobSet upid, then pressidSort in ascending order, otherwise save in order to a queue, will remove duplicatesjob.job1Nested in thejob2Will be executed immediately
  • queuePreFlushCbReceiving a functioncbAs parameters, other properties andqueueJobThe same
  • queuePostFlushCbReceiving a functioncbOr an array of functionscbsAs parameters, other properties andqueueJobThe same
  • queuePreFlushCbqueueJobqueuePostFlushCbThey can call each other and execute immediately
  • If the nestedpreFlushCbThe parent is called again injobThen the call will be skipped
  • injob1In the callqueueJob(job2)job2In thequeuePostFlushCbIn the andqueueJob(job2)At the same level ofqueuePostFlushCbExecute after execution
  • invalidateJobYou can have a guyjobDoes not perform
  • nextTickCatch errors
  • Recursion is not allowed by defaultjobEtc., unless specifiedallowRecursetrue
  • flushPostFlushCbsWill makequeuePostFlushCbThe recursion in is performed only once
  • Priority:queuePreFlushCb > queueJob > queuePostFlushCb
  • ifjobactiveProperties forfalse, thenjobWill not be enforced

The source code parsing

The data structure

The data structure of the scheduled task SchedulerJob

export interface SchedulerJob extendsFunction { id? :numberactive? :booleancomputed? :booleanallowRecurse? :booleanownerInstance? : ComponentInternalInstance }Copy the code
  • id: To keep tasks unique, the tasks in the queue pressidAscending order
  • active: Indicates whether the task is executed
  • computed
  • allowRecurse: Whether to allow recursive calls to itself
  • ownerInstance

Queue of tasks to be scheduled

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

Data structures for two classes of four callback functions

// The callback function queue in the asynchronous task queue before the task executes
const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

// Queue of callback functions after task execution in asynchronous task queue
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0
Copy the code

nextTick

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

We can simplify the code to make it easier to understand

const p = Promise.resolve()
export function nextTick(fn? : () = >void) :Promise<void> {
  return fn ? p.then(fn) : p
}
Copy the code

Use promise.resolve (). Then to convert fn to a microtask and join the microtask queue

QueueJob Queued asynchronous tasks

export function queueJob(job: SchedulerJob) {
  if((! queue.length || ! queue.includes( job, isFlushing && job.allowRecurse ? flushIndex +1: flushIndex, )) && job ! == currentPreFlushParentJob ) {if (job.id == null) {
      queue.push(job)
    } else {
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}
Copy the code

By default, the search starts at the current task, which means recursive calls and repeated additions are not allowed

If job. AllowRecurse is true, the search position is incremented by one, and no recursion is allowed.

Then, according to whether the job. Id attribute is available, the task is sorted last or in ascending order by ID. This ensures that the tasks are sorted correctly in ascending order by ID when the queue is refreshed

Finally, queueFlush() is called to process the queue

QueuePreFlushCb/queuePostFlushCb processes the callback

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

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

You can see that queuePreFlushCb and queuePostFlushCb are encapsulation of queueCb. The only difference between them is the parameters that are passed in. Let’s look at queueCb:

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{ pendingQueue.push(... cb) } queueFlush() }Copy the code

The enqueueing logic is basically the same as the processing of asynchronous tasks. On the one hand, we de-weight, and on the other hand, we handle the recursive logic according to the configuration. Alternatively, if the callback is an array, it is the component’s lifecycle hook function. This set of functions can only be called by asynchronous tasks and has been de-duplicated. So I’m just going to flatten the array to one dimension and push it into the pendingQueue. This part is Vue’s own design.

QueueFlush pushes the microtask queue

After joining the team, we struggled to start working on asynchronous tasks. Let’s start with two global variables that control the refresh logic:

let isFlushing = false
let isFlushPending = false
Copy the code

Here, we flushJobs into the engine’s microtask queue if there are no tasks waiting or executing:

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

function queueFlush() {
  if(! isFlushing && ! isFlushPending) { isFlushPending =true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
Copy the code

This ensures that you can add tasks multiple times in a tick. At the same time, the engine must call flushJobs in the microtask queue once after executing the function of the main call stack.

FlushJobs Processes asynchronous tasks

We passed

resolvedPromise.then(flushJobs)
Copy the code

By adding flushJobs to the microtask queue, flushJobs will be executed when the engine processes the next microtask queue

Let’s take a look at the timing of the correction:

type CountMap = Map<SchedulerJob | SchedulerCb, number>
function flushJobs(seen? : CountMap) {
  isFlushPending = false
  isFlushing = true
  // ...
  flushPreFlushCbs(seen)

  // Handle asynchronous tasks

  flushPostFlushCbs(seen)
  isFlushing = false
}
Copy the code

It is actually through these two functions that the callback function queue is executed.

In addition, we need to sort the task queue once before processing the asynchronous task queue, so that the tasks in the queue are sorted in ascending order by ID

const getId = (job: SchedulerJob): number= >
  job.id == null ? Infinity : job.id!
function flushJobs(seen? : CountMap) {
  flushPreFlushCbs(seen)
  queue.sort((a, b) = > getId(a) - getId(b))
  // Handle asynchronous tasks
}
Copy the code

There are two reasons for doing this. The comments on the source code say so:

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.

Translation, mainly to ensure two things:

  • The component update sequence is from parent to child (because the parent component is always created before the child component, the parent component has smallerid, i.e., higher priority)
  • If a component is unloaded during the update of its parent, its update can be skipped

Now let’s look at the asynchronous task processing part

The main code is as follows:

for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
  const job = queue[flushIndex]
  if(job && job.active ! = =false) {
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
 }
Copy the code

Traverse the queue and perform the tasks

Some asynchronous tasks also add new asynchronous tasks as they are executed, so we will execute them as well

if (queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length) {
  flushJobs(seen)
}
Copy the code

FlushPreFlushCbs Handles the callback before an asynchronous task

export function flushPreFlushCbs(seen? : CountMap, parentJob: SchedulerJob |null = null.) {
  if (pendingPreFlushCbs.length) {
    currentPreFlushParentJob = parentJob
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    pendingPreFlushCbs.length = 0

    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      activePreFlushCbs[preFlushIndex]()
    }
    activePreFlushCbs = null
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // recursively flush until it drains
    flushPreFlushCbs(seen, parentJob)
  }
}
Copy the code

The logic is clear: it iterates through the activePreFlushCbs queue and executes the functions in turn. Note that the last recursive call to flushPreFlushCbs handles the recursion. The queue may change during recursion, so we made a copy of the queue before processing it:

activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
Copy the code

FlushPostFlushCbs Processes the callback after an asynchronous task is processed

export function flushPostFlushCbs(seen? : CountMap) {
  if (pendingPostFlushCbs.length) {
    const deduped = [...new Set(pendingPostFlushCbs)]
    pendingPostFlushCbs.length = 0

    // #1947 already has active queue, nested flushPostFlushCbs call
    if(activePostFlushCbs) { activePostFlushCbs.push(... deduped)return
    }

    activePostFlushCbs = deduped

    activePostFlushCbs.sort((a, b) = > getId(a) - getId(b))

    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      activePostFlushCbs[postFlushIndex]()
    }
    activePostFlushCbs = null
    postFlushIndex = 0}}Copy the code

FlushPostFlushCbs and flushPreFlushCbs have much the same logic. FlushPostFlushCbs also handles nesting, making nested functions run once

if(activePostFlushCbs) { activePostFlushCbs.push(... deduped)return
}
Copy the code

That’s this use case

// #1947 flushPostFlushCbs should handle nested calls
// e.g. app.mount inside app.mount
test("flushPostFlushCbs".async() = > {let count = 0

  const queueAndFlush = (hook: Function) = > {
    queuePostFlushCb(hook)
    flushPostFlushCbs()
  }

  queueAndFlush(() = > {
    queueAndFlush(() = > {
      count++
    })
  })

  await nextTick()
  expect(count).toBe(1)})Copy the code

In addition, when flushJob calls flushPostFlushCbs, it resets isFlushing to false. This is to handle newly added asynchronous tasks. FlushJob continues recursively until all asynchronous tasks are processed, if any.

flushPostFlushCbs(seen)
isFlushing = false
Copy the code

conclusion

In general, the implementation of nextTick mainly uses

  • usingPromise.resolve().then()Push the task inMicro Task QueueWith the help of the engineEvent LoopMechanism processes tasks in queues
  • Processing of asynchronous tasks and callbacks, as well as recursive processing of newly added asynchronous tasks. This is handled with the engineTask QueueThe logic is consistent

refs

Vue3 source code analysis: nextTick

Vue3 source reading notes (six) — nextTick and scheduler