[Vue] Explore nextTick
This article is based on VUE 2.6.11 source code. The implementation of Vue 3.0 has changed slightly, but the ideas can still be borrowed from 2.6. The 3.0 changes will be covered at the end of the article.
I’ve had a lot of questions about nextTick:
-
How does this function perform a callback after a DOM update?
-
In reactive data modification, there is a phenomenon that all modifications are merged and unified. How to achieve this phenomenon?
We have the response data a, b, a is equal to'to modify a'; NextTick (print dom corresponding to a and B) b ='modify b';// The control console prints a and b corresponding to the DOM are modified, how to do this? Copy the code
We answer them all:
Question 1:
This is an interesting thing to say, and it has to do with the order of the calls. If the order of the calls is correct, it is possible to execute the callback before dom updates, for example:
data() {
return {
content: 'before'}},mounted() {
this.test()
},
methods: {
test() {
this.nextTick(() = > {
console.log(this.$refs.box.innerHTML) // called before modifying reactive data
})
this.content = 'after'
this.nextTick(() = > {
console.log(this.$refs.box.innerHTML)
})
}
}
// Print the result:
// before
// after
Copy the code
Ok, so now that we know that this stuff is sequentially related, we need some kind of data structure to hold the order of the calls.
Let’s go to see the source: github.com/vuejs/vue/b…
Read the notes in the order β – > β₯
let pending = false // Prevent the timerFunc function from being executed repeatedly
const callbacks = [] NextTick (callback); nextTick(callback)
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
callbacks.push(() = > { // insert the callback into the array
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
if(! pending) { pending =true // Because timerFunc is an asynchronous call, the timerFunc function may be called repeatedly without control. From this we can also see that while timerFunc is waiting in the asynchronous queue, the callbacks array will accumulate the functions passed in when nextTick is called.
timerFunc() FlushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks = flushCallbacks}...// There is some code that is not relevant to this article, delete it
}
timerFunc = () = > { (4) This function pushes the flushCallbacks into the asynchronous call stack, regardless of whether it uses setTimeout or promise. Then or MutationObserver or setImmediate
setImmediate(flushCallbacks) // See below ππ»
}
function flushCallbacks () { // call the array callbacks
pending = false // The callbacks will be copied and emptied, so there is no need to prevent repeated calls to timerFunc
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() // β₯ Execute the function saved in callbacks!}}Copy the code
Ok, now we know how the call-by-call callback function is implemented.
But here comes a new problem: ππ»ππ» disk
How does modifying responsive data relate to nextTick? I didn’t call it!
This is related to the responsive principle, let’s look at the source code:
Github.com/vuejs/vue/b…
As we all know, the reactive principle of VUE is similar to the published-subscribe model. For each reactive data, there is a corresponding event center, in which a bunch of watcher will register and wait for the notification of data change, so let’s simulate:
The this.content = ‘after’ assignment is intercepted by the setter and notified once, at which point watcher.update() is called.
The update function calls queueWatcher, another function.
QueueWatcher executes this line nextTick(flushSchedulerQueue). The flushSchedulerQueue is responsible for executing all the collected Watcher.
OK, found out where to call nextTick!
The source code is as follows:
Github.com/vuejs/vue/b…
/** * Subscriber interface. * Will be called when a dependency changes. */ is called when a dependency changes
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)// Call call call call call call call call call}}Copy the code
Github.com/vuejs/vue/b…
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if(! flushing) { queue.push(watcher)// β οΈβ οΈβ οΈ pushes all watchers into a queue for continuous execution. β οΈ β οΈ β οΈ
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// queue the flush
if(! waiting) { waiting =true.// There are some irrelevant code, delete
nextTick(flushSchedulerQueue) // This is where nextTick is called}}}Copy the code
Question 2: In reactive data modification, all changes are merged and unified. How is this implemented?
There are two steps to modify responsive data:
Step1 collect the watcher with a queue.
Step2 asynchronously call the flushSchedulerQueue function to clear the queue.
Step1 queueWatcher collection operation depends on the function, just in a question at the end of π π» π π» π π» π π» π π» π π» π π» π π» π π», see β οΈ annotations.
FlushSchedulerQueue (flushSchedulerQueue) {flushSchedulerQueue (flushSchedulerQueue);
Github.com/vuejs/vue/b…
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort((a, b) = > a.id - b.id)
// β οΈ executes all collected watcherβ οΈ at once
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
... // There are some irrelevant code, delete}}Copy the code
summary
This is the end of the source code analysis, the responsive principle of the source code is not here to continue in depth, and then write off the topic.
If you are interested, you can search for the following content in vue2.6 source code, with related articles, and explore by yourself:
export function defineReactive // src/core/observer/index.jsFor the component responsive object //src/core/observer/index.js
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val/ *eslint-disable no-self-compare* /if (newVal === value || (newVal ! == newVal && value ! == value)) {
return
}
/* eslint-enable no-self-compare */
if(process.env.NODE_ENV ! = ='production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Setter intercepts assignment and fires a notification
}
// src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if(process.env.NODE_ENV ! = ='production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) = > a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // Call the update function to form a closed loop}}Copy the code
Is there a change in the implementation of NextTick in Vue3.0?
There are! Extract a piece of source code. As you can see, use promise.then() to store & concatenate the various callback functions to ensure the order of the calls. Or just return a promise and let the user control with async/await
// https://github.com/vuejs/vue-next/blob/44996d1a0a2de1bc6b3abfac6b2b8b3c969d4e01/packages/runtime-core/src/scheduler.ts#L 42
export function nextTick(
this: ComponentPublicInstance | void, fn? : () = >void
) :Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
Copy the code
NextTick is based on macro tasks or micro tasks
This is dependent on the runtime environment. In the source code, the implementation is wrapped as the function timerFunc before it is used. So, at the code level, the nextTick function doesn’t care whether the timerFunc implementation is a microtask or a macro task. In Vue 3, the implementation of 2.6 has been abandoned in favor of promise.then(), a component chain structure.
You can read the 2.6.11 source code: github.com/vuejs/vue/b…