This is the fifth day of my participation in the August Wen Challenge.More challenges in August
Case analysis
Get the updated DOM
It is common to change a DOM and then read the value of the DOM with vUE, but the result is often not what we expected due to the vUE’s asynchronous updating mechanism, such as this case:
<p id="msg">{{message}}</p>
data: {
message: '123465',}clickBtn() {
this.message = 'hello'
let msg = document.querySelector('#msg').innerText // Message is reassigned, expecting hello
console.log(msg) // Actual output 123465
}
Copy the code
How to solve it? Using nextTick
clickBtn(){
this.message = 'hello'
this.nextTick(() = >{
let msg = document.querySelector('#msg').innerText // Message is reassigned, expecting hello
console.log(msg) // Actually prints hello})}Copy the code
Source code analysis
Instead of immediately updating the view when it hears data changes, it starts a queue to buffer the Watcher, triggers the same Watcher multiple times, pushes the Watcher to the queue only once, and finally calls nextTick to refresh the queue to execute the Watcher
update
Vue decides to update according to different conditions. In general, I adopts an asynchronous update strategy, traversing the subs property of the subscriber Dep object, which contains the list of Watcher. In asynchronous update, queueWatcher() is used to put the Watcher into an update queue
// core/observe/watcher.js
update() {
/* istanbul ignore else */
if (this.lazy) {
/ / lazy loading
this.dirty = true
} else if (this.sync) {
// Synchronize execution
this.run()
} else {
// Execute asynchronously
queueWatcher(this) // Put the current Watcher into the update queue}}Copy the code
queueWatcher
The Watcher is placed in the queue property in an orderly manner, and only one Watcher can be updated at a time
// core/observe/scheduler.js
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
// Assign weight according to id,
if (has[id] == null) {
has[id] = true
if(! flushing) {// Not flushing the Watcher queue
queue.push(watcher) // Push the Watcher to the queue. There is only one Watcher with the same ID
} else {
// It has been updated, put this watcher next to the current watcher, and execute the inserted watcher as soon as the current watcher completes execution
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1.0, watcher)
}
// queue the flush
// While nextTick is locked, watcher can be pushed to queue
// Wait until the flushSchedulerQueue executes to unlock the flushSchedulerQueue and restore the state for the next nextTick
if(! waiting) { waiting =true
if(process.env.NODE_ENV ! = ='production' && !config.async) {
// Not asynchronous refresh
flushSchedulerQueue() // Update the queue immediately
return
}
nextTick(flushSchedulerQueue) // Update asynchronously}}}Copy the code
flushSchedulerQueue
The watcher.run() function iterates through the queue to be updated and executes watcher.run() on each Watcher in the queue.
// core/observe/scheduler.js
function flushSchedulerQueue() {
currentFlushTimestamp = getNow() // Get the current timestamp
flushing = true // Set to refresh
let watcher, id
// Component updates from parent to child. (Because the parent is always created before the child node)
// The component's user monitor runs before its render monitor (because the user observer is created before the render observer)
// If a component is destroyed while the parent's monitor is running, its observer can be skipped.
queue.sort((a, b) = > a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index] / / remove the Watcher
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run() // Update the Watcher in the queue
// in dev build, check and stop circular updates.
// Update up to 100 times to prevent infinite loops
if(process.env.NODE_ENV ! = ='production'&& has[id] ! =null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break}}}// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice() // Copy the active component
const updatedQueue = queue.slice() // Copy the updated queue
resetSchedulerState() // Resets the state of the refresh queue for the next refresh
// Call Component updated and activated hooks // Lifecycle hook functions
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')}}Copy the code
vm.nextTick
Push the callback function into the Callbacks array and cache it
// core/util/next-tick.js
export function nextTick(cb? :Function, ctx? :Object) {
let _resolve
callbacks.push(() = > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
// Lock timerFunc, call nextTick multiple times, and place cb callbacks directly into the Callbacks array
// timerFunc is executed after all nextTick is executed
if(! pending) { pending =true
timerFunc() // How do callback letters work, Promise? SetTimeout method? MutationObserver way? SetImmediate way? Compatible with various environments
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
// Simply return a Promise object if no callback function is returned
return new Promise((resolve) = > {
_resolve = resolve
})
}
}
Copy the code
timerFunc
Finally, the timerFunc() function iterates through the callbacks cache and executes, mainly deciding how to refresh the callback: Promise, setTimeout, and so on. To take just one example,
// core/util/next-tick.js
let timerFunc = () = > {
setTimeout(flushCallbacks, 0)}function flushCallbacks() {
pending = false // Clear the flag
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() Execute the function in callbacks that nextTick carries with it}}Copy the code
conclusion
The general process of asynchronous rendering: Trigger multiple Watcher –> Enter the ququeWatcher–> reassign by ID –> push to queue –> Trigger nextTick–> push the flushSchedulerQueue into the callbacks with closure –> enter timerFunc to iterate callbacks The function of
Always remembernextTick
Asynchronous operation
clickBtn() {
this.$nextTick(() = > {
console.log('nextTick1')})console.log(2)
this.$nextTick(() = > {
console.log('nextTick2')})console.log(1)}// Output: 2 1 nextTick1 nextTick2
Copy the code
For example, now operate continuously on a property this. MSG =1; This. MSG =2vue Instead of updating this property twice in a row, as might be expected, only perform the last time according to the asynchronous rendering mechanism to avoid meaningless operation loss of performance
In addition, using the nextTick method multiple times in a row does not immediately execute the contents of nextTick, which is rare. Reading the source code above, it caches the nextTick callback into the callbacks array, and finally executes the operations through the number
clickBtn(){
this.$nextTick(() = > {
console.log('nextTick1')})this.$nextTick(() = > {
console.log('nextTick2')})}Copy the code
The articles
- [Vue source]–$nextTick and asynchronous rendering (line by line comments)
- [Vue source]–mixin and extend (line by line)
- [Vue source]–Diff algorithm (line by line comment)
- [Vue source]– How to listen for array changes (line by line comment)
- [Vue source]– Reactive (bidirectional binding) principle (line by line comment)
- [Vue source]– what is done for each lifecycle (line by line comments)
- [Vue source]– How to generate virtual DOM (line by line comment)