Vue uses data detection to detect changes in state. As mentioned in the previous article “How Vue implements data detection”, when data is updated, such as when executing this.title = ‘Listen to whether I have changed’, A call to dep.notify in the setter function notifies Watcher to perform an update (specifically, the watcher.update function). Then, when Vue creates Watcher, how to schedule Watcher queue through Scheduler, and how the update of Watcher is finally reflected in view rendering, this paper mainly introduces the implementation principle of Vue Watcher based on these three questions.

1. When to create Watcher

Components go through a series of life cycles from creation to destruction. BeforeMount, Mounted, beforeUpdate, updated are familiar. Knowing the life cycle makes it much easier to understand when Watcher was created. There are three places in Vue where Watcher objects are created: mount event, $watch function, computed, and watch property. Mount event creates Watcher for rendering notifications, and Watch and computed Watcher are both used to listen for user-defined property changes.

1.1 mount events

File core/instance/lifecycle. Js contains a Vue life cycle of related functions, such as $forupdate, $destroy and instantiate the Watcher mountComponent function, The mountComponent function fires when $mount is executed after the component is mounted. This function first fires the beforeMount hook event. The before function is passed when Watcher is instantiated, which triggers the beforeUpdate hook. When a component has property updates, Watcher fires the beforeUpdate event before the update (watcher.run). IsRenderWatcher indicates that a render Watcher is created and hangs directly on the vm._watcher property. When $forceUpdate is forced to refresh the render, vm._watcher.update is executed to trigger the rendering process and the corresponding update hook.

@param {*} vm * @param {*} el * @param {*} hydrating * @returns */ export function mountComponent ( vm: Component, el: ? Element, hydrating? : boolean ): Component { vm.$el = el callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), Hydrating)} // Instantiate the Watcher object, New Watcher(VM, updateComponent, noop, Before () {if (vm._isMounted &&! Vm._isdestroyed) {callHook(vm, 'beforeUpdate')}} isRenderWatcher Watcher.update} is manually triggered when $forceupdate is executed, true /* isRenderWatcher */) return vm } export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options? :? Object, isRenderWatcher? : boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) this.getter = expOrFn this.value = this.lazy ? undefined : this.get() } } Vue.prototype.$forceUpdate = function () { const vm: Component = this if (vm._watcher) { vm._watcher.update() } }Copy the code

1.2 $watch function

In the component, in addition to using watch and computed methods to monitor attribute changes, Vue defines the $watch function to monitor attribute changes. For example, when A.B.C nested attribute changes, $watch can be used to monitor and perform subsequent processing. $watch is equivalent to writing the function of watch attribute directly in the component. For example, the Keep-alive component in Vue source code uses $watch to monitor include and exclude attributes in Mounted events.

$watch(expOrFn, callback, [options]) {string | Function} expOrFn {Function | Object} callback {Object} {Boolean} [options] deep {Boolean} immediate return values: $watch(' A.B.C ', Function (newVal, // Keep-alive. js file Mounted () {this.cachevNode () this.$watch('include', val => {pruneCache(this, pruneCache); // Keep-alive. js file mounted () {this.cachevNode () this. name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => ! matches(val, name)) }) }Copy the code

The difference between the $watch function and the mountComponent function is that the mountComponent function is used to render listeners, which triggers relevant hook events, whereas the $Watch function is more specialized, dealing with expOrFn listeners. $watch(‘name’, ‘nameChange’); $watch(‘name’, ‘nameChange’); The nameChange function of the Vue entity is triggered if the name is updated.

/ / to monitor properties change Vue. Prototype. $watch = function (expOrFn: string | function, cb: any options? : Object ): Function { const vm: Component = this // cb may be a pure JS object, Handler if (isPlainObject(cb)) {return createWatcher(VM, expOrFn, cb, options) } const watcher = new Watcher(vm, expOrFn, cb, Return function unwatchFn () {watcher.teardown()}} function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options? : Object) {// When the function is an Object, If (isPlainObject(handler)) {options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }Copy the code

1.3. Watch and computed attributes

These two attributes must be familiar when developing components using Vue, for example, using Watch to define listening for firstName and secondName attributes, and using computed to define listening for fullName attributes. When firstName and secondName are updated, fullName also triggers the update.

new Vue({
  el: '#app',
  data() {
    return {
        firstName: 'Li',
        secondName: 'Lei'
    }
  },
  watch: {
      secondName: function (newVal, oldVal) {
          console.log('second name changed: ' + newVal)
      }
  },
  computed: {
      fullName: function() {
          return this.firstName + this.secondName
      }
  },
  mounted() {
    this.firstName = 'Han'
    this.secondName = 'MeiMei'
  }
})
Copy the code

When we define listening on properties in Watch and computed, when does Vue convert it to a Watcher object for listening? Vue constructor is called _init (options) performs initialization, source core/components/instance/init. Js file defines _init function, performed a series of initialization, such as initialization state of life cycle, events, etc., The initState function contains initializations for watch and computed.

/ / core/components/instance/init. Js / / Vue constructor function Vue (options) {enclosing _init (options)} / / core/components/instance/init.js Vue.prototype._init = function (options? : Object) { initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') } // // core/components/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options ... if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch ! == nativeWatch) { initWatch(vm, opts.watch) } }Copy the code

1.3.1 computed properties

InitComputed Initializes computed attributes, and each Vue entity contains the _computedWatchers object watcher object that stores all computed attributes. First, through computed objects, a new Watcher object is created for each key, and its lazy property is true, which means that Watcher caches the calculated values. If the dependent properties (such as firstName and secondName) are not updated, Current computed attributes (such as fullName) also do not trigger updates. Attributes defined in computed can be accessed through this(such as this.fullname), and defineComputed mounts all computed attributes to the Vue entity.

// Const computedWatcherOptions = {lazy: true} function initComputed (VM: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) for (const key in computed) { const userDef = Computed [key] // User-defined performing functions may be of the form {get: function() {}} const getter = typeof userDef === 'function'? userDef : UserDef. Get / / for the user to define each computed properties create watcher object which [key] = new watcher (vm, getter | | it, it, Computed WatcherOptions) // The computed properties of the component itself are already defined on the component prototype chain, so we just need to define the instantiated computed properties. // For example, if we define fullName in computed, defineComputed attaches it to the attributes of a Vue object if (! (key in vm)) { defineComputed(vm, key, userDef) } }Copy the code

The defineComputed function converts the calculated property to {get, set}, but the calculated property doesn’t need a set, so the code assigns it the noOP null function directly. The get function that calculates attributes is encapsulated by createComputedGetter. It first finds the watcher object of the corresponding attribute. If the dirty value of watcher is true, it indicates that the dependent attribute has been updated and needs to be recalculated with the evaluate function.

// Convert computed attributes to {get, set} and hook them to a Vue entity so that export Function defineComputed (target: any, key: string, userDef: Object | Function ) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? createComputedGetter : noop sharedPropertyDefinition.set = userDef.set || noop } Object.defineProperty(target, key, SharedPropertyDefinition)} function createComputedGetter (key) {return function ComputedGetter () {// _computedWatchers Defines a Watcher object for each computed attribute const Watcher = this._computedWatchers && This._computedwatchers [key] if (watcher) {// Dirty = true, Watcher.evaluate ()} if (dep.target) {// Append dep.target (watcher) to the dependency of the current watcher watcher.depend() } return watcher.value } } }Copy the code

If dep.target has a value, attach other dependent Watcher(such as dependent Watcher used to fullName) to the Dep collection of the property that the currently calculated property depends on. The following code creates a listener for the fullName computed property, which we’ll name watcher3. All deP objects in firstName and secondName have watcher3 observers attached to them, and any change in their properties will trigger watcher3’s update function to re-read the fullName value.

$watch('fullName', function (newVal, oldVal) {// do something})Copy the code

1.3.2 watch attributes

The initWatch function is relatively simple, iterating over the dependencies for each property, or if the dependencies are arrays, creating a separate Watcher observer for each dependency. As mentioned earlier, the createWatcher function uses $watch to create a new Watcher entity.

Function initWatch (VM: Component, Watch: Object) {for (const key in watch) {const handler = watch[key] Watcher if (array.isarray (handler)) {for (let I = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }Copy the code

2.Scheduler schedules processing

Vue in core/observer/scheduler. Js file defines the scheduling function, a total of two use, Watcher object and core/vdom/create – component. Js file. When an update is performed, the Watcher object is attached to a scheduling queue for execution. While create-component.js deals with the rendering process, scheduler is used to trigger activated hook events. Watcher’s use of Scheduler is highlighted here. When watcher’s update function is executed, all watcher (except lazy and sync) will attach the queueWatcher function to the scheduling queue.

Export default class Watcher {/** * if the dependency is updated, /* Istanbul ignore else */ if (this.lazy) {this.dirty = true} else if (this.sync) {this.run() */ Istanbul ignore else */ if (this.lazy) {this.dirty = true} else if (this.sync) {this.run() } else { queueWatcher(this) } } }Copy the code

QueueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher: queueWatcher For the difference between microtasks and macrotasks, see reference 8 “Differences between macroTasks and microtasks”. If the flushSchedulerQueue is not yet executed (flushing is false), simply append the watcher to the queue. Otherwise, you need to check the execution progress of the current micro task. The queue is sorted in ascending order by the ID of the watcher, ensuring that the watcher created first is executed first. Index is the watcher index that is being executed in the microtask. The watcher will be inserted at a position greater than index and in ascending order of ID. Finally, the queue execution function flushSchedulerQueue creates a microtask from nextTick to be executed.

/* * Append watcher to queue, skip if there is a duplicate watcher. * if the queue is running (flushing is true), put the watcher in place */ export function queueWatcher (watcher: // All watchers have an incremented unique identifier, const id = Watcher. Id // If Watcher is already in the queue, If (has[id] == null) {has[id] = true if (! Push (watcher)} else {// If it is running, append it to the appropriate location based on the id. // index is the watcher index that is currently being executed, and the watcher before index has been executed. // The watcher created first should be executed first, compared with the watcher in the queue, and inserted into the appropriate position. Let I = queue.length-1 while (I > index && queue[I].id > watcher.id) {I --} // I = 1; Watcher [i-1].id < watcher[I].id < watcher[I + 1].id queue.splice(I + 1, 0, watcher)} // If the queue is not queued, nextick will execute the queue.  if (! waiting) { waiting = true nextTick(flushSchedulerQueue) } } }Copy the code

NextTick will select a microtask execution queue that is appropriate for the current browser, such as MutationObserver, Promise, and setImmediate. The flushSchedulerQueue function flushSchedulerQueue iterates through all watchers and performs updates. The queue is sorted in ascending order to ensure that the first watcher is executed first. For example, the parent watcher takes precedence over the child watcher. The queue is then traversed, triggering watcher’s before function. For example, mountComponent creates watcher with a before event, triggering callHook(VM, ‘beforeUpdate’). The update (watcher.run) operation is then performed. When the queue is finished, the resetSchedulerState function is called to clear the queue and reset the execution state. Finally, callActivatedHooks and callUpdatedHooks trigger the corresponding Activated and Updated hooks events.

*/ function flushSchedulerQueue () {currentFlushTimestamp = getNow() flushing = true Id // Sort the queue before traversing it // Sort the queue to ensure: // 1. The parent component is updated before the child component, because the parent component must be created before the child component. // 2. Component custom Watcher will be executed before render Watcher because custom Watcher is created before Render Watcher. // 3. If the component destroyed wtcher during the parent component's execution, its Watcher collection can be skipped. Queue.sort ((a, b) => a.id -b.id) // Do not cache length, because the queue is always adjusting while wacher is traversed. for (index = 0; index < queue.length; Index++) {watcher = queue[index] if (watcher.before) { For example, run the beforeUpdated hook watcher.before()} id = watcher.id has[id] = null // Run the update watcher watcher.run()} // Since the activatedChildren and Queue queues are constantly updated, Slice () const updatedQueue = queue.slice() // resets the dropped queue state ResetSchedulerState () // Trigger activated and updated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue)}Copy the code

3. Update Watcher

Each watcher has an active state, indicating whether the watcher is currently active. When the component executes $destroy, the teardown function of the watcher is called to set active to false. Before executing the update notification callback cb, there are three criteria. First, check whether the values are equal. If value is an object or deep traversal is required (deep is true), for example, if the user defines the person attribute, its value is the object {age: number, sex: Number}, we use $watch(‘person’, cb) to listen for the person property, but cb will not be executed when the Person. age changes. If you change it to $watch(‘person’, cb, {deep: true}), cb will be triggered if any nested attributes change. If one of the three conditions is met, the CB callback will be triggered.

Export default class Watcher {/** * the scheduler will execute */ run () {// Update notification will be executed only when Watcher is active. If (this.active) {// Call get to get the value const value = this.get() if (// If the newly calculated value updates the value! = = this. Value | | / / if the value for the object or array, regardless of the value and this value is equal to no, Its depth, which should also be triggered / / because its nested attribute may change isObject (value) | | this. Deep) {const oldValue = this. The value this. Cb. The call (enclosing the vm, Value, oldValue)}}}} this.$watch('person', () => {this.message = 'age :' + this.person.age}, // When deep is true, when age is updated, A callback will be triggered; If deep is false,age updates do not trigger a callback {deep: true})Copy the code

The run function has a call to get the latest value. In the get function, pushTarget is called to append the current Watcher to the global DEP. target, and getter is executed to get the latest value. In the finally module, if deep is true, traverse recursion is called to traverse the latest value, which can be Object or Array, so you need to traverse the child property and trigger its getter, Append its DEP attribute to dep. target(the current Watcher) so that any change in the value of the subattribute is notified to the current Watcher. For why, please refer back to our previous article on How Vue detects Data state.

Export default class Watcher {/** * execute getter to collect dependencies */ get () {// Append the current Watcher to the global dep.target, PushTarget (this) let value const vm = this.vm try {// Execute getter to read value value = this.gett. call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, 'Getter for watcher "${this.expression}"')} else {throw e}} finally {// If deep is true, // The deP for all nested attributes is appended to the current watcher, and the DEP for all child attributes is recursively iterated through all nested attributes and fires their getters from push(dep.target) if (this.deep) {// Traverse (value)} // Exit stack popTarget() // Clean up dependency this.cleanupdeps ()} return value}}Copy the code

Traverse recursive traversal subproperties in get functions can be illustrated with practical examples, such as {person: {age: 18, sex: 0, addr: {city: ‘Beijing ‘, detail: }}, Vue will call observe to convert person to Observer. The subproperties (if they are objects) will also be converted to Observer. Simple properties will define get and set functions.

When the watcher. Get traverse is performed, the child properties will be recursively traversed, and when the addr property is traversed, the get function will be triggered, which will call its dep.depend to append the current Watcher to the dependency, so that when executing this.person.age = 18, The set function calls dep.notify to trigger watcher’s update function, which implements the monitoring of the Person object.

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    ...
  }
  return value
}

set: function reactiveSetter (newVal) {
  ...
  dep.notify()
}
Copy the code

4. To summarize

In this article, we introduce the process from the creation of Watcher to the final component rendering. Then, we introduce three parts in detail: “When to create Watcher”, “Scheduler schedule processing” and “Watcher update “. “When Watcher is created” shows where Vue creates Watcher objects, “Scheduler dispatches processing “shows how Vue performs updates to the Wacher queue through microtasks, and “Watcher Updates” briefly describes how each Watcher performs updates. For reasons of space, how watcher’s update will eventually translate into view rendering will be covered in the next article. It’s a little long, but it shows you love Vue.

reference

1. The Schduler synchronous and asynchronous calculation, blog. Windstone. Cc/vue/source -… 2.Vue asynchronous update queue, cn.vuejs.org/v2/guide/re… 3. Vue reactive process, segmentfault.com/a/119000001… 4. The correct use of Vue refresh mechanism, michaelnthiessen.com/force-re-re… 5. Dynamic components and asynchronous components, cn.vuejs.org/v2/guide/co… 6. IsPlainObject function ohhoney1. Making. IO/Vue. Js – Sour… 7. The nature of computed Tomography in Vue, juejin.cn/post/684490… 8. The difference between macroTask and microTask, javascript.info/event-loop#…

Write in the last

If you have other questions can be directly message, discuss together! \color{red}{if you have any other questions, please leave a message. } if you have other questions can be directly message, discuss together! Recently I will continue to update Vue source code introduction, front-end algorithm series, interested in a wave of attention. \color{red}{recently I will continue to update Vue source code introduction, front-end algorithm series, interested can pay attention to a wave. } recently I will continue to update the Vue source code introduction, front-end algorithm series, interested can pay attention to a wave.