This is the 23rd day of my participation in Gwen Challenge
Vue.nexttick ([callback, context]) : {Function} [callback]
{Object} [context] usage: Executes a deferred callback after the next DOM update loop. Use this method immediately after modifying the data to get the updated DOM.Copy the code
1. Give examples
<template>
<div>
<div ref="msg">{{msg}}</div>
<div v-if="msg1">out $nextTick: {{msg1}}</div>
<div v-if="msg2">in $nextTick: {{msg2}}</div>
<div v-if="msg3">out $nextTick: {{msg3}}</div>
<button @click="changeMsg"> changeMsg </button>
</div>
</template>
<script>
export default {
data(){
return{
msg: 'Hello Vue'.msg1: ' '.msg2: ' '.msg3: ' '}},methods: {
changeMsg() {
this.msg = "Hello world"
this.msg1 = this.$refs.msg.innerHTML
this.$nextTick(() = > {
this.msg2 = this.$refs.msg.innerHTML
})
this.msg3 = this.$refs.msg.innerHTML
}
}
}
</script>
Copy the code
After the click:
As you can see from the figure above, msG1 and MSG3 still display the content before the transformation, while MSG2 displays the content after the transformation. The root cause is that DOM updates in Vue are asynchronous.
2. Data change dom update and principle analysis of nextTick
2.1 Data Changes
Vue bidirectional data binding relies on ES5’s Object.defineProperty, which creates getters and setters for each property during data initialization to turn data into responsive data. Modifying a property value, such as this. MSG = Hello world, actually fires the setter.
Data changes trigger the set function
Object.defineProperty(obj, key, {
enumerable: true.configurable: true.// The set function is triggered to update the DOM through a series of operations
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if(getter && ! setter)return
if (setter) {
setter.call(obj, newVal)
} else{ val = newVal } childOb = ! shallow && observe(newVal) dep.notify()// Run the dep notify method}})Copy the code
The dep.notify method is executed
export default class Dep {
constructor() {
this.id = uid++
this.subs = []
}
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
// The update method is actually iterated over the elements in the subs array
subs[i].update()
}
}
}
Copy the code
When the data is referenced, such as <div>{{MSG}}</div>, the get method is executed and the render Watcher is added to the subs array. When the data is changed, Watcher's update method is executed to update the data.Copy the code
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) / / queueWatcher execution}}Copy the code
The update method finally executes queueWatcher
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if(! flushing) { queue.push(watcher) }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) {// Ensure that nextTick executes only once through waiting
waiting = true
The queueWatcher method finally passes the flushSchedulerQueue into nextTick for execution
nextTick(flushSchedulerQueue)
}
}
}
Copy the code
Execute the flushSchedulerQueue method
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// Iterate through the render Watcher's run method to complete the view update
watcher.run()
}
// Reset the waiting variable
resetSchedulerState()
...
}
Copy the code
That is, when a data change finally passes the flushSchedulerQueue into nextTick, executing the flushSchedulerQueue will iterate through the watcher.run() method, The watcher.run() method eventually does the view update, so let’s see what the key nextTick method is.
2.2 nextTick
The nextTick method is pushed into the Callbacks array by the incoming callback, and the timerFunc method is executed
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
// Push into the callbacks array
callbacks.push(() = > {
cb.call(ctx)
})
if(! pending) { pending =true
// Execute the timerFunc method
timerFunc()
}
}
Copy the code
timerFunc
let timerFunc
// Determine whether to support Promise natively
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
// Use the Promise to execute the flushCallbacks if native Promise is supported
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// Determine whether MutationObserver is supported natively
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// Use MutationObserver to flushCallbacks if native MutationObserver is supported
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// Determine whether setImmediate is native supported
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
timerFunc = () = > {
SetImmediate Executes flushCallbacks with setImmediate if native supports setImmediate
setImmediate(flushCallbacks)
}
// Use setTimeout 0 when neither is supported
} else {
timerFunc = () = > {
// use setTimeout to flushCallbacks
setTimeout(flushCallbacks, 0)}}FlushCallbacks finally executes the callback passed in by the nextTick method
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
NextTick uses microTask in preference to macroTask.
That is, tasks in nextTick actually execute asynchronously, and nextTick(callback) is similar to promise.resolve ().then(callback), or setTimeout(callback, 0).
In other words, vue’s view update nextTick(flushSchedulerQueue) is the same as setTimeout(flushSchedulerQueue, 0), and will execute flushSchedulerQueue asynchronously. So we don’t update the DOM immediately when this. MSG = Hello world.
To read DOM information after the DOM update, we need to create an asynchronous task after the creation of the asynchronous task.
To test this idea, let’s use setTimeout instead of nextTick. The following code verifies our idea.
<template>
<div class="box">{{msg}}</div>
</template>
<script>
export default {
name: 'index',
data () {
return {
msg: 'hello'
}
},
mounted () {
this.msg = 'world'
let box = document.getElementsByClassName('box') [0]
setTimeout(() = > {
console.log(box.innerHTML) // world}}})Copy the code
If we nextTick before the data is modified, the asynchronous task we add will be executed before the rendered asynchronous task and the updated DOM will not be retrieved.
<template>
<div class="box">{{msg}}</div>
</template>
<script>
export default {
name: 'index',
data () {
return {
msg: 'hello'
}
},
mounted () {
this.$nextTick(() = > {
console.log(box.innerHTML) // hello
})
this.msg = 'world'
let box = document.getElementsByClassName('box') [0]}}Copy the code
3. Application scenarios
- DOM operations performed by the Created () hook function in the Vue lifecycle must be placed in the vue.nexttick () callback
The DOM doesn’t actually render at the time the Created () hook function executes, and DOM manipulation is useless, so make sure you put the DOM manipulation js code in the view.nexttick () callback. The mounted() hook function is the mounted() hook function, because it executes when all DOM operations are completed.
- An operation to be performed after data changes that requires the use of a DOM structure that changes with the data should be placed in the vue.nexttick () callback.