Vue asynchronous update mechanism

Vue asynchronous update mechanism

Preface

Because the current project uses Vue.js, I am also looking at the Vue.js source code recently. Based on this, the following summary and output are made. This chapter is mainly about the analysis of Vue's asynchronous update mechanism and source code implementation.

dep.notify

According to the reactive principle of Vue, when the setter method of a certain data is triggered, its setter function will notify Dep in the closure, and Dep will call all watcher objects it manages. Trigger the update implementation of the watcher object.

dep.notify

/src/core/observer/dep.js

//Notify all watchers in dep to execute the update method in watcher 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) } //Traverse the watcher stored in dep and execute watcher.update() for ( let i = 0 , l = subs.length; i <l; i++) { subs[i].update() } } Copy code

watcher.update

/src/core/observer/watcher.js

/** * Subscriber interface. * Will be called when a dependency changes. * Decide which process to go according to the watcher configuration item, usually queueWatcher */ update () { /* istanbul ignore else */ if ( this .lazy) { //When lazy execution, follow the logic, such as compted //set dirty to true, after the component is updated, when the responsive data is updated again, execute computed getter //Re-execute the computed callback function, calculate the new value, and cache it to watcher.value this .dirty = true } else if ( this .sync) { //Directly execute the run function to render the view when executed synchronously this .run() } else { //Put the watcher into the watcher queue queueWatcher( this ) } } Copy code

queueWatcher

/src/core/observer/scheduler.js

/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. * Put the watcher into the watcher queue */ export function queueWatcher ( watcher: Watcher ) { const id = watcher.id //If the watcher already exists, it will be skipped and will not be placed in the queue repeatedly if (has[id] == null ) { //Cache watcher .id is used to determine whether the watcher has been enqueued has[id] = true if (!flushing) { //Currently there is no flushing of the queue status, then the watcher is directly placed in the queue queue.push(watcher) } else { //Already refreshing the queue status, according to the current watcher.id to find the position of watcher.id greater than it, and then insert yourself to the next position after the position //That is to put the current watcher into the queue, And keep the queue in order //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 if (process.env.NODE_ENV !== 'production' && !config.async) { //If it is a non-production environment and set asynchrony to false, execute synchronously flushSchedulerQueue() return } /** * The frequently used nextTick is this.$nextTick Vue.nextTick * 1. Put the callback function flushSchedulerQueue into the callbacks array * 2. Add the flushCallbacks function to the browser task queue through pending control */ nextTick(flushSchedulerQueue) } } } Copy code

It can be seen from the queueWatcher code that the watcher object does not update the view immediately, but is pushed into a queue queue. At this time, the state is in the waiting state. At this time, watcher objects will continue to be pushed into the queue queue, and wait until the next one. When the tick is running, all the queues are taken out and run again, these watcher objects will be traversed out and the view will be updated. At the same time, watchers with duplicate ids will not be added to the queue multiple times. This also explains that the same watcher is triggered multiple times and will only be pushed into the queue once. Based on this, we can understand the entire process according to the following figure:

In order to avoid frequent manipulation of the DOM, Vue uses an asynchronous method to update the DOM. These asynchronous operations will use the nextTick function to put these operations in the task queue in the form of cb (with microtask first), and execute these cbs after each tick to update the DOM.

nextTick

const callbacks = [] let pending = false /** * 1. Use try catch to wrap the cb function, and then put it into the callbacks array * 2. Judge the pending value, if it is false, it means that there is no flushCallbacks function being executed in the current browser task queue * If pening is true, it means that the flushCallbacks function has been placed in the browser task queue * The role of pending: to ensure that at the same time, there is only one flushCallbacks function in the browser's task queue * @param {*} cb receives a callback function * @param {*} The context in which the ctx callback function is executed * @returns */ export function nextTick ( cb?: Function , ctx?: Object ) { let _resolve //Put the incoming callback function into the callbacks array callbacks.push( () => { if (cb) { //Wrap the callback function with try catch to facilitate error catching try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick' ) } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true //Execute timerFunc and put the flushCallbacks function in the browser's task queue (preferred microtask queue) //Mainly used to determine the priority of Promise, MutationObserver, setImmediate, and setTimeout timerFunc() } //$flow-disable-line if (!cb && typeof Promise !== 'undefined' ) { return new Promise ( resolve => { _resolve = resolve }) } } Copy code

timerFunc

/src/core/util/next-tick.js

//You can see that the function of timerFunc is very simple, that is, put the flushCallbacks function into the browser's asynchronous task queue let timerFunc //Judgment 1: Whether Promise is natively supported if ( typeof Promise !== 'undefined' && isNative( Promise ) ) { const p = Promise .resolve() //Preferred Promise.resolve().then() timerFunc = () => { //Put the flushCallbacks function in the microtask queue p.then(flushCallbacks) /** * In problematic UIWebViews, Promise.then will not be completely interrupted, but it may fall into a weird state, * In this state, the callback is pushed into the micro task queue, but the queue is not refreshed until the browser needs to perform other work, such as processing a timer. * Therefore, we can "force" refresh the microtask queue by adding an empty timer. */ if (isIOS) setTimeout (noop) } isUsingMicroTask = true //Judgment 2: Whether MutationObserver is natively supported } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || //PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { //MutationObserver second //Use MutationObserver where native Promise is not available, //eg PhantomJS, iOS7, Android 4.4 //(#6466 MutationObserver is unreliable in IE11) let counter = 1 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 //Judgment 3: Whether setImmediate is natively supported } else if ( typeof setImmediate !== 'undefined' && isNative(setImmediate)) { //Then there is setImmediate, which is actually a macro task, but it is still better than setTimeout better timerFunc = () => { setImmediate(flushCallbacks) } //Judgment 4: None of the above works, directly use setTimeout } else { //Finally, if there is no way, use setTimeout timerFunc = () => { setTimeout (flushCallbacks, 0 ) } } Copy code

flushCallbacks

/src/core/util/next-tick.js

const callbacks = [] //pending is used to identify that it can only be executed once at the same time let pending = false /** * Set pending to false * Clear the callbacks array * Execute each function in the callbacks array */ function flushCallbacks () { pending = false const copies = callbacks.slice( 0 ) callbacks.length = 0 for ( let i = 0 ; i <copies.length; i++) { copies[i]() } } Copy code

flushSchedulerQueue

/src/core/observer/scheduler.js

/** * Flush both queues and run the watchers. * Flush the queue, which is called by the flushCallbacks function, and mainly does the following two things: * 1. Update flushing to true, which means that the queue is being refreshed. During this period, a new watcher needs to be specially processed when pushing a new watcher into the queue (place it in a suitable position in the queue) * 2. Sort by watcher.id in the queue from small to large, and ensure that the watcher created first is executed first, and also cooperate with the first step * 3. Traverse the watcher queue, execute watcher.before, watcher.run in turn, and clear the cached watcher */ function flushSchedulerQueue () { currentFlushTimestamp = getNow() //The flag is now flushing the queue flushing = true let watcher, id /** * Sort the queue (ascending order) before refreshing the queue to ensure: * 1. The update order of components is from parent to child, because the parent component is always created before the child component * 2. The user watcher of a component is executed before it renders the watcher, because the user watcher is created before the rendering watcher * 3. If a component is destroyed during the execution of the watcher of its parent component, its watcher can be skipped * After sorting, new watchers that come in during the refresh of the queue will also be placed in the appropriate position of the queue in order */ queue.sort( ( a, b ) => a.id-b.id) //The queue.length is used directly here, the length of the queue is dynamically calculated, there is no buffer length, because the queue may be pushed into the new watcher during the execution of the existing watcher for (index = 0 ; index <queue.length; index++) { watcher = queue[index] //Execute before hook, when using vm.$watch or watch option, you can pass if (watcher.before) { watcher.before() } //Clear the cached watcher id = watcher.id has[id] = null //Execute watcher.run, and finally trigger the update function, such as updateComponent or get this.xx (xx is the second parameter of the user watch). Of course, the second parameter may also be a function, then execute it directly watcher.run() } //keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() /** * Reset scheduling status: * 1. Reset the has cached object, has = {} * 2, waiting = flushing = false, indicating that the flushing queue is over * waiting = flushing = false, which means that you can put a new flushSchedulerQueue function in the callbacks array, and you can put the next flushCallbacks function in the browser's task queue */ resetSchedulerState() //call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) //devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit( 'flush' ) } } /** * Reset the scheduler's state. */ function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production' ) { circular = {} } waiting = flushing = false } Copy code
/** * Called by the flush queue function flushSchedulerQueue, if it is a synchronous watch, it is called directly by this.update to complete the following things: * 1. Execute the second parameter passed by the instantiation watcher, updateComponent or a function to get this.xx (the function returned by parsePath) * 2. Update the old value to the new value * 3. The third parameter passed when the watcher is instantiated, such as the callback function of the user watcher */ run () { if ( this .active) { //call this.get method const value = this .get() if ( value !== this .value || //Deep watchers and watchers on Object/Arrays should fire even //when the value is the same, because the value may //have mutated. isObject(value) || this .deep ) { //Update the old value to the new value const oldValue = this .value this .value = value if ( this .user) { //If it is a user watcher, execute the third parameter passed by the user-the callback function, the parameters are val and oldVal try { this .cb.call( this .vm, value, oldValue) } catch (e) {handleError (e, this .vm, `callback for watcher " ${ this .expression} "` ) } } The else { //Render watcher, this.cb = noop, an empty function the this .cb.call ( the this .vm, value, oldValue) } } } } Copy code

summary

The core of Vue's asynchronous update mechanism is realized by using the browser's asynchronous task queue. The micro task queue is the first choice, and the macro task queue is second.

  1. The traversal attribute adds get and set methods to it. The dependencies (dev.subs.push(watcher)) are collected in the get method, and the set method calls the notify method of dep. The function of this method is to notify all collected in dep The watcher and call the update method of the watcher, we can understand this as publish and subscribe in the design pattern.

  2. By default, the update method will trigger the queueWatcher function after being called. The main function of this function is to add the watcher instance itself to a queue (queue.push(watcher)), and then call nextTick(flushSchedulerQueue).

  3. Then put a method to flush the watcher queue (flushSchedulerQueue) into a global callbacks queue through the nextTick method, and then traverse and execute the callbacks asynchronously (this is an asynchronous update queue). If there is not a function called flushCallbacks in the asynchronous task queue of the browser at this time, the timerFunc function is executed and the flushCallbacks function is placed in the asynchronous task queue. If the flushCallbacks function already exists in the asynchronous task queue, wait for its execution to complete before placing the next flushCallbacks function.

  4. flushSchedulerQueue is a function whose purpose is to call the watcher.run method of all watchers in the queue to enter the update phase. After the run method is called, the next operation is to generate a new virtual DOM through the diff algorithm of the new virtual DOM and the old virtual DOM. Real DOM

  5. As mentioned above, flushSchedulerQueue calls watcher.run() after being executed, so you see a new page

reference

Vue source code analysis
easy-to-understand Vue asynchronous update mechanism