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
queueJob
Receiving a functionjob
As a parameter, ifjob
Set upid
, then pressid
Sort in ascending order, otherwise save in order to a queue, will remove duplicatesjob
.job1
Nested in thejob2
Will be executed immediatelyqueuePreFlushCb
Receiving a functioncb
As parameters, other properties andqueueJob
The samequeuePostFlushCb
Receiving a functioncb
Or an array of functionscbs
As parameters, other properties andqueueJob
The samequeuePreFlushCb
、queueJob
、queuePostFlushCb
They can call each other and execute immediately- If the nested
preFlushCb
The parent is called again injob
Then the call will be skipped - in
job1
In the callqueueJob(job2)
,job2
In thequeuePostFlushCb
In the andqueueJob(job2)
At the same level ofqueuePostFlushCb
Execute after execution invalidateJob
You can have a guyjob
Does not performnextTick
Catch errors- Recursion is not allowed by default
job
Etc., unless specifiedallowRecurse
为true
flushPostFlushCbs
Will makequeuePostFlushCb
The recursion in is performed only once- Priority:
queuePreFlushCb
>queueJob
>queuePostFlushCb
- if
job
的active
Properties forfalse
, thenjob
Will 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 pressid
Ascending orderactive
: Indicates whether the task is executedcomputed
:allowRecurse
: Whether to allow recursive calls to itselfownerInstance
:
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:
- Components are updated from parent to child. (because parent is always created before the child so its render effect will have smaller priority number)
- 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 smaller
id
, 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
- using
Promise.resolve().then()
Push the task inMicro Task Queue
With the help of the engineEvent Loop
Mechanism 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 engine
Task Queue
The logic is consistent
refs
Vue3 source code analysis: nextTick
Vue3 source reading notes (six) — nextTick and scheduler