NextTick Introduction
According to the brief introduction on the official website, nextTick
it is a tool method to wait for the next DOM update refresh.
The type definition is as follows:
function nextTick(callback?: () => void): Promise<void> {}
Then according to the detailed introduction on the official website, we can know nextTick
the general implementation ideas and usage:
When you
Vue
change reactive state in a , the finalDOM
updates are not synchronized, but areVue
cached in a queue until the next "tick". This is to ensure that each component only performs one update regardless of how many state changes occur.
nextTick()
Can be used immediately after a status change to wait forDOM
the update to complete. You can pass a callback function as argument, orawait
return itPromise
.
The explanation on the official website is already very detailed, so I won’t over-interpret it. The next step is the analysis.
Some details and usage of nextTick
Usage of nextTick
First, according to the introduction on the official website, we can know that nextTick
there are two ways to use it:
- Pass in callback function
nextTick(() => {// DOM 更新了
})
- Return a
Promise
nextTick().then(() => {// DOM 更新了
})
So can these two methods be mixed?
nextTick(() => {// DOM 更新了
}).then(() => {// DOM 更新了
})
nextTick phenomenon
I wrote a very simple one demo
and found that it can be mixed, and discovered an interesting phenomenon:
const {createApp, h, nextTick} = Vue;
const app = createApp({data() {return {count: 0};},methods: {push() {nextTick(() => {console.log('callback before');}).then(() => {console.log('promise before');});this.count++;nextTick(() => {console.log('callback after');}).then(() => {console.log('promise after');});}},render() {console.log('render', this.count);const pushBtn = h("button", {innerHTML: "增加",onClick: this.push});const countText = h("p", {innerHTML: this.count});return h("div", {}, [pushBtn, countText]);}
});
app.mount("#app");
I use it here for simplicity
vue.global.js
. The usage method isVue3
the same, but it isESM
introduced without using it.
The running results are as follows:
In my example, clicking the add button will count
perform an increment operation. This method can be divided into three parts:
1. Use nextTick
and use Promise
the mixed use of the callback function and
2. count
Add one to the pair
3. Use nextTick
and use Promise
the mixed use of the callback function and
The first registered one is executed nextTick
before count
adding one, and the second registered one is executed nextTick
after count
adding one.
But the final result is very interesting:
callback before
render 1
promise before
callback after
promise after
The first registered nextTick
callback function is render
executed before, Promise
but render
after.
The second registered nextTick
, callback function is render
executed after, Promise
but render
after.
And both nextTick
callback functions take precedence over Promise
execution.
How to explain this phenomenon? We will nextTick
start with the implementation of the analysis.
Implementation of nextTick
nextTick
There are only more than 200 lines of source code in packages/runtime-core/src/scheduler.ts
the file. If you are interested, you can go directly to ts
the source code of the version. We will still look at the packaged source code.
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {const p = currentFlushPromise || resolvedPromise;return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
At first glance, people are dumbfounded. nextTick
There is only such a small amount of code? Taking a closer look, we find nextTick
that the implementation is actually an Promise
encapsulation.
Ignoring other things for the moment, just look at this code, we can know:
nextTick
What is returned is aPromise
nextTick
The callback function is executed in the methodPromise
ofthen
Now going back to what we said before demo
, we have actually found part of the answer:
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
The final execution sequence above, expressed in code, is:
function nextTick(fn) {// 2. 返回一个 Promise, 并且在 Promise 的 then 方法中执行回调函数return Promise.resolve().then(fn);
}
// 1. 调用 nextTick,注册回调函数
const p = nextTick(() => {console.log('callback before');
})
// 3. 在 Promise 的 then 方法注册一个新的回调
p.then(() => {console.log('promise before');
});
// 4. 执行 count++
this.count++;
From the disassembled code, what we can see is:
nextTick
What is returned is aPromise
nextTick
The callback function is executed in the methodPromise
ofthen
According to Promise
the characteristics of , we know Promise
that it can be called in a chain, so we can write like this:
Promise.resolve().then(() => {// ...
}).then(() => {// ...
}).then(() => {// ...
});
And according to the characteristics of , a new one is Promise
returned every time ;Promise
Promise
At the same time, we also know Promise
that then
the method is executed asynchronously, so we have some guesses about the execution order of the above code, but we can't draw a conclusion now, let's continue to dig deeper.
Implementation details of nextTick
Although the above source code is very short, there is a currentFlushPromise
variable in it, and this variable is let
declared using. All variables are const
declared using. This variable is let
used to declare, and it must be in stock.
Through search, we can find where this variable is used and find that there are two methods of using this variable:
queueFlush
: willcurrentFlushPromise
be set to aPromise
flushJobs
: will becurrentFlushPromise
set tonull
queueFlush
// 是否正在刷新
let isFlushing = false;
// 是否有任务需要刷新
let isFlushPending = false;
// 刷新任务队列
function queueFlush() {// 如果正在刷新,并且没有任务需要刷新if (!isFlushing && !isFlushPending) {// 将 isFlushPending 设置为 true,表示有任务需要刷新isFlushPending = true;// 将 currentFlushPromise 设置为一个 Promise, 并且在 Promise 的 then 方法中执行 flushJobscurrentFlushPromise = resolvedPromise.then(flushJobs);}
}
In fact, these codes are easy to understand without writing comments. You can know the meaning by seeing the name. In fact, you can already get a glimpse of it here:
queueFlush
Is a method used to refresh the task queueisFlushing
Indicates whether it is refreshing, but it is not used in this method.isFlushPending
Indicates whether there are tasks that need to be refreshed, which are queued tasks.currentFlushPromise
Indicates tasks that currently need to be refreshed
Now combined with the above nextTick
implementation, we will actually find a very interesting point. resolvedPromise
Both of them are in use:
const resolvedPromise = Promise.resolve();
function nextTick(fn) {// nextTick 使用 resolvedPromise return resolvedPromise.then(fn);
}
function queueFlush() {// queueFlush 也使用 resolvedPromisecurrentFlushPromise = resolvedPromise.then(flushJobs);
}
Simplifying the above code, it actually looks like this:
const resolvedPromise = Promise.resolve();
resolvedPromise.then(() => {// ...
});
resolvedPromise.then(() => {// ...
});
In fact, the method is used Promise
to then
register multiple callback functions, and all tasks that need to be refreshed are registered in the same Promise
method then
, so that the execution order of these tasks can be guaranteed, which is a queue.
flushJobs
In the above queueFlush
method, we know that queueFlush
it is a method used to refresh the task queue;
So what tasks should be refreshed? Anyway, the last thing passed in is a flushJobs
method, and it is also used in this method currentFlushPromise
. Isn’t this just a string? Let’s take a look:
// 任务队列
const queue = [];
// 当前正在刷新的任务队列的索引
let flushIndex = 0;
// 刷新任务
function flushJobs(seen) {// 将 isFlushPending 设置为 false,表示当前没有任务需要等待刷新了isFlushPending = false;// 将 isFlushing 设置为 true,表示正在刷新isFlushing = true;// 非生产环境下,将 seen 设置为一个 Mapif ((process.env.NODE_ENV <img src="https://github.com/evanw/esbuild/issues/1610)const check = (process.env.NODE_ENV !== 'production')? (job) => checkRecursiveUpdates(seen, job): NOOP;// 检测递归调用是一个非常巧妙的操作,感兴趣的可以去看看源码,这里不做讲解try {for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex];if (job && job.active !== false) {if ((process.env.NODE_ENV !== 'production') && check(job)) {continue;}// 执行任务callWithErrorHandling(job, null, 14 /* ErrorCodes.SCHEDULER */);}}}finally {// 重置 flushIndexflushIndex = 0;// 快速清空队列,直接给 数组的 length属性 赋值为 0 就可以清空数组queue.length = 0;// 刷新生命周期的回调flushPostFlushCbs(seen);// 将 isFlushing 设置为 false,表示当前刷新结束isFlushing = false;// 将 currentFlushPromise 设置为 null,表示当前没有任务需要刷新了currentFlushPromise = null;// pendingPostFlushCbs 存放的是生命周期的回调,// 所以可能在刷新的过程中又有新的任务需要刷新// 所以这里需要判断一下,如果有新添加的任务,就需要再次刷新if (queue.length || pendingPostFlushCbs.length) {flushJobs(seen);}" style="margin: auto" />
}
flushJobs
First, it will isFlushPending
be set to false
, the current batch of tasks has already started to be refreshed, so there is no need to wait, and then it will be isFlushing
set to true
, indicating that it is refreshing.
This queueFlush
is exactly the opposite of the method, but their functions complement each other. queueFlush
It means that there is currently a task that requires attributes, and flushJobs
it means that the task is currently being refreshed.
The execution of tasks is callWithErrorHandling
performed through methods. The code inside is very simple, which is to execute the method and capture errors during the execution, and then hand over the errors to onErrorCaptured
the method for processing.
The refresh tasks are stored in queue
attributes. This queue
is the task queue we mentioned above. What is stored in this task queue is the task we need to refresh.
Finally, clear queue
and execute flushPostFlushCbs
the method. flushPostFlushCbs
The method usually stores life cycle callbacks, such as mounted
, updated
etc.
Task addition to queue
As mentioned above queue
, queue
how do you add tasks?
Through search, we can locate queueJob
the method, which is used to add tasks:
// 添加任务,这个方法会在下面的 queueFlush 方法中被调用
function queueJob(job) {// 通过 Array.includes() 的 startIndex 参数来搜索任务队列中是否已经存在相同的任务// 默认情况下,搜索的起始索引包含了当前正在执行的任务// 所以它不能递归地再次触发自身// 如果任务是一个 watch() 回调,那么搜索的起始索引就是 +1,这样就可以递归调用了// 但是这个递归调用是由用户来保证的,不能无限递归if (!queue.length ||!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) {// 如果任务没有 id 属性,那么就将任务插入到任务队列中if (job.id == null) {queue.push(job);}// 如果任务有 id 属性,那么就将任务插入到任务队列的合适位置else {queue.splice(findInsertionIndex(job.id), 0, job);}// 刷新任务队列queueFlush();}
}
Here job
is a function, which is the task we need to refresh, but this function will expand some attributes, such as id
, pre
, active
etc.
There is a pair of type definitions in ts
the source code job
:
export interface SchedulerJob extends Function {// id 就是排序的依据id?: number// 在 id 相同的情况下,pre 为 true 的任务会先执行// 这个在刷新任务队列的时候,在排序的时候会用到,本文没有讲解这方面的内容pre?: boolean// 标识这个任务是否明确处于非活动状态,非活动状态的任务不会被刷新active?: boolean// 标识这个任务是否是 computed 的 gettercomputed?: boolean/** * 表示 effect 是否允许在由 scheduler 管理时递归触发自身。 * 默认情况下,scheduler 不能触发自身,因为一些内置方法调用,例如 Array.prototype.push 实际上也会执行读取操作,这可能会导致令人困惑的无限循环。 * 允许的情况是组件更新函数和 watch 回调。 * 组件更新函数可以更新子组件属性,从而触发“pre”watch回调,该回调会改变父组件依赖的状态。 * watch 回调不会跟踪它的依赖关系,因此如果它再次触发自身,那么很可能是有意的,这是用户的责任来执行递归状态变更,最终使状态稳定。 */allowRecurse?: boolean/** * 在 renderer.ts 中附加到组件的渲染 effect 上用于在报告最大递归更新时获取组件信息。 * 仅限开发。 */ownerInstance?: ComponentInternalInstance
}
queueJob
The method first determines queue
whether the same task already exists. If the same task exists, there is no need to add it again.
This mainly deals with the problem of recursive calls, because most of the tasks stored here are triggered when we modify the data;
When modifying data, array methods are used, such as forEach
, map
etc. These methods will be triggered when executed, getter
and the method getter
will be triggered in turn queueJob
, which will lead to recursive calls.
So it will be judged here isFlushing
, if it is refreshing, then it will be flushIndex
set to +1
;
flushIndex
It is the index of the task currently being refreshed, +1
and then the search starts from the next task, so that the same task will not be added repeatedly and cause recursive calls.
The watch
callback can be called recursively, because this is controlled by the user, so there is an additional allowRecurse
attribute here. If it is watch
a callback, it will be allowRecurse
set to true
.
This can avoid the problem of recursive calls, which is a very clever design.
queueJob
The last one is exported, this is used for other modules to add tasks, such as watchEffect
, watch
etc.
flushPostFlushCbs
flushPostFlushCbs
Methods are used to execute life cycle callbacks, such as mounted
, updated
etc.
flushPostFlushCbs
I won’t go into too much detail, the overall process is flushJobs
pretty much the same;
The difference is that flushPostFlushCbs
the tasks will be backed up and then executed sequentially, and exceptions will not be caught, but will be called directly.
Interested students can check the source code themselves.
The beginning of the problem
Going back to the original question, which is demo
the example at the beginning of the article, let’s review demo
the code first:
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
nextTick(() => {console.log('callback after');
}).then(() => {console.log('promise after');
});
The printed result is:
callback before
render 1
promise before
callback after
promise after
In fact, it is very clear by looking through the source code. nextTick
When we registered the first one, queue
there was no task in it;
And the method nextTick
will not be called queueJob
, nor will flushJobs
the method be called, so the task queue will not be refreshed at this time.
But resolvedPromise
it is a success promise
, so nextTick
the callback function passed in will be placed in the microtask queue, waiting for execution.
nextTick
One will also be returned promise
, so the callback function we return will also be placed in the microtask queue, but it will definitely lag behind promise
the callback function.then
nextTick
Then we execute it this.count++
. We haven’t touched the internal implementation logic here yet. We only need to know that it will trigger queueJob
the method and add the task to the task queue.
Finally, we executed it again nextTick
. At this time, queue
there was already a task, so flushJobs
the method was called to execute the tasks in the task queue in sequence.
Emphasis: And currentFlushPromise
there is a value at this time. The value is resolvedPromise
returned after the execution is completed Promise
.
The difference from the first time is that the first time it was executed nextTick
, currentFlushPromise
it undefined
used resolvedPromise
;
It can be understood that the first nextTick
time it is executed, the flushJobs
same task as the method registration is used Promise
.
When executed for the second time nextTick
, the task used is not the same as currentFlushPromise
the one registered by the method .Promise
flushJobs
Promise
This ensures that nextTick
the registered callback function will flushJobs
be executed after the method's registered callback function.
The specific process can be seen in the following code example:
const resolvedPromise = Promise.resolve();
let count = 0;
// 第一次注册 nextTick
resolvedPromise.then(() => {console.log('callback before', count);
}).then(() => {console.log('promise before', count);
});
// 执行 this.count++
// 这里会触发 queueJob 方法,将任务添加到任务队列中
const currentFlushPromise = resolvedPromise.then(() => {count++;console.log('render', count);
});
// 第二次注册 nextTick
currentFlushPromise.then(() => {console.log('callback after', count);
}).then(() => {console.log('promise after', count);
});
You can execute the result of the above code in your browser and you will find that it is consistent with our expectations.
The specific process can be seen in the figure below:
graph TD
A[resolvedPromise] -->|注册 nextTick 回调| B[nextTick callback before]
B -->|在 nextTick 返回的 promise 注册 then 的回调| C[nextTick promise then]
A -->|执行 value++ 会触发 queueJob| D[value++]
D -->|执行 flushJobs 会将 resolvedPromise 返回的 promise 赋值到 currentFlushPromise| E[currentFlushPromise]
E -->|注册 nextTick 回调使用的是 currentFlushPromise| F[nextTick callback after]
F -->|在 nextTick 返回的 promise 注册 then 的回调| G[nextTick promise after]
The above synchronized macro task has been executed, and the next step is the micro task queue. The process is as follows:
graph TD
A[resolvedPromise] -->|直接调用 then 里面注册的回调函数| B[then callbacks]
B -->|注册了多个,依次执行| C[nextTick callback before]
C -->|注册了多个,依次执行| D[value++]
In this way, the second wave of tasks is also over. This time the task is mainly to refresh the task queue. What is executed here nextTick
is actually the previous task tick
(now you understand 直到下一个“tick”才一起执行
what it means on the official website).
Then execute the next one tick
(that’s what I mean, manual dog head), the process is as follows:
graph TD
A[nextTick promise then] -->|因为是先注册的,所以先执行| B[nextTick promise before]
It’s over, yes, the task this time is to execute the nextTick
returned callback function;promise
then
Because nextTick
the returned sums promise
are currentFlushPromise
not the same promise
, what is nextTick
returned is a single task, and the priority is higher .promise
then
currentFlushPromise
After this mission is over, there will be another one tick
. The process is as follows:
graph TD
A[currentFlushPromise then] -->|因为是后注册的,所以相对于上面的后执行| B[nextTick callback after]
This time the task is to execute currentFlushPromise
the then
callback function, and it is also a call flushJobs
, which flushJobs
is assigned resolvedPromise
the returned Promise
value currentFlushPromise
.
This mission is over and it is the last one tick
. The process is as follows:
graph TD
A[nextTick promise after] -->|最后一个| B[nextTick promise after]
At this point, the process is over. The process is very brain-consuming, but after understanding it, I found it to be very clever. It has greatly improved my thinking ability and my understanding of asynchronous.
Summarize
This article mainly analyzes the implementation principles. By analyzing the source code, we found Vue3
that the implementation principles are very clever.nextTick
nextTick
nextTick
The implementation principle is Promise
implemented by , nextTick
which will return one Promise
, and nextTick
the callback function will be placed in the microtask queue, waiting for execution.
If registered while there are tasks queued nextTick
, nextTick
the callback function will be executed after the tasks in the task queue are executed.
The idea used here is very simple, which is to take advantage Promise
of the chainable call feature. Everyone may have used it in daily development, but I didn't expect it to be used in this way. It is really very clever.
at last
We have compiled 75 JS high-frequency interview questions, and provided answers and analysis. This can basically guarantee that you can cope with the interviewer's questions about JS.
Friends in need can click on the card below to receive it and share it for free