preface

Last Sunday, Vue.2. X source code responsive principles talked about Vue responsive principles by setting getters and setters for each key of an Object, thus intercepting access to data. The entry point for an asynchronous update is in the setter’s dep.notify() method.

The source code interpretation

dep.notify

src/core/observer/dep.js

/** * notify all watchers in dep, Update () */ 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() } }Copy the code
watcher.update

src/core/observer/watcher.js

/** * Subscriber interface. * Will be called when a dependency changes. ** QueueWatcher */ update () {/* Istanbul ignore else */ if (this.lazy) {// for example, for computed // set dirty to true, } else if (this.sync) {// execute synchronously, You can pass a sync option when using vm.$watch or the watch option. // When true, the watcher will not go to the asynchronous update queue when data is updated. // this property does not appear in the official document this.run()} else {// if you want to update this property, put watcher in the watcher queue queueWatcher(this)}}Copy the 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. */ export function queueWatcher (watcher: If (has[id] == null) {// cache watcher.id, Has [id] = true if (! Flushing) {// not currently in the flushing queue, Push (watcher)} else {// if already flushing, splice the watcher based on its id // if already past its id, // Start from the end of the queue in reverse order, select watcher.id from the current watcher.id, select watcher.id from the current watcher.id, select watcher.id from the current watcher.id, select watcher.id from the current watcher.id, select watcher.id from the current watcher.id. Then insert itself into the next position after that position // put the current watcher into the sorted queue, Let I = queue.length - 1 while (I > index && queue[I].id > watcher.id) {I --} queue.splice(I + 1, 0, 0, 0) watcher) } // queue the flush if (! waiting) { waiting = true if (process.env.NODE_ENV ! == 'production' && ! Config.async) {// refresh the schedule queue directly. // Vue is async by default. FlushSchedulerQueue () return} nextTick(flushSchedulerQueue)}}}Copy the code
nextTick

src/core/util/next-tick.js

Const callbacks = [] let pending = false /** * * 1, use the try catch flushSchedulerQueue packaging function, and then put it in the callbacks array * 2, if the pending to false, * If pending is true, flushCallbacks are already in the browser's task queue. * If pending is true, flushCallbacks are already in the browser's task queue. Pending will be set to false again, indicating that the next flushCallbacks can be added to the browser's task queue. Make sure that at the same time, The browser has only one flushCallbacks function in the task queue * @param {*} cb to receive a callback function => flushSchedulerQueue * @param {*} CTX context * @returns */ 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) } }) if (! Pending) {pending = true // Execute timerFunc, Put the flushCallbacks function timerFunc()} in the browser's task queue (preferred microtask queue); // $flow-disable-line if (! cb && typeof Promise ! == 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }Copy the code
timeFunc

src/core/util/next-tick.js

// You can see that timerFunc is simple, Let timerFunc // The nextTick behavior leverages The microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, However it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ if (typeof Promise ! == 'undefined' && isNative(Promise)) {const p = promise.resolve () timerFunc = () => {// Preferred promise.then p.then(flushCallbacks) // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (! isIE && typeof MutationObserver ! == 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. 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 } else  if (typeof setImmediate ! == 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task Queue, // but it is still a better choice than settimeout.timerfunc = () => {// But it's still better than setTimeout. SetImmediate (flushCallbacks)}} else {// In the end, // Fallback to settimeout.timerFunc = () => {setTimeout(flushCallbacks, 0)}}Copy the code
flushCallbacks

src/core/util/next-tick.js

Const callbacks = [] let pending = false /** * Set pending to false * 2; clear the callbacks array * 3; execute each function in the callbacks array (flushSchedulerQueue) () {pending = false const copies = callbacks. Slice (0) callbacks. Length = 0 FlushSchedulerQueue for (let I = 0; i < copies.length; i++) { copies[i]() } }Copy the code
flushSchedulerQueue

src/core/observer/schedule.js

/** * Flush both queues and run the watchers. * flushCallbacks (); * 1, update the flushing of the queue when the new watcher is in the queue * 2, update the flushing of the queue when the new watcher is in the queue * 2, update the flushing of the queue when the new watcher is in the queue * 3. Traverse the watcher queue and run the watcher.before and watcher.run commands successively. Function flushSchedulerQueue () {currentFlushTimestamp = getNow() flushing = true let flushSchedulerQueue () id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers 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] if (watcher.before) {watcher.before()} id = watcher.id has[id] = null // execute Watcher.run, which eventually triggers an update function, such as updateComponent, or gets this.xx (where xx is the second argument to the user's watch). The second argument could also be a function, Run () // in dev build, check and stop circular updates. 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() const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } } function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted &&! vm._isDestroyed) { callHook(vm, 'updated') } } }Copy the code
watcher.run()

src/core/observer/watcher.js

/** * flushSchedulerQueue (); /** * flushSchedulerQueue (); * 1, execute the second argument passed by watcher, UpdateComponent or get a function of this.xx (returned by parsePath) * 2, update the old value to the new value * 3, execute the third argument passed when instantiating watcher, */ run () {if (this.active) {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 ) { // set new value const oldValue = this.value this.value = value if (this.user) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { this.cb.call(this.vm, value, oldValue) } } } }Copy the code
watcher.get()

src/core/observer/wathcer.js

/** * Evaluate the getter, and re-collect dependencies. * Execute this. Getter, and re-collect dependencies. A function or string, such as: updateComponent or a function returned by parsePath that reads the value of this.xx property * why should the dependencies be collected again? * because trigger updates on how responsive data has been updated, but is updated data although already after observe observation, but not to rely on collection, * so, in the update page, will perform a render function again, during execution of triggering read operations, */ get() {pushTarget(this) let value const vm = this.vm try {value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value }Copy the code

conclusion

Vue’s asynchronous update mechanism is involved here, which is mainly implemented through the browser’s asynchronous task queue, preferentially microtask queue and then macro task queue.

Call dep.notify() after the reactive data has been updated to notify the watcher collected by DEp to call the update method. Watcher.update will place the watcher in the watcher queue, the global queue array

NextTick flushSchedulerQueue, a method to flush the Watcher queue, into a global array of Callbacks

If the browser does not have flushCallbacks in the asynchronous queue at this time, then the TimeFunc function is executed to put flushCallbacks in the asynchronous queue. If an asynchronous task has a flushCallback function in the queue and it finishes executing, put the next flushCallback function in the queue

The flushCallback function executes all flushSechedulerQueue functions in the Callbacks array

FlushSchedulerQueue responsible for refresh watcher queue, that executes the queue array each watcher in the run method, thus entered a stage of update.