With the reading of the vue source code, I gradually found that Watcher is everywhere. Whether it is the principle of responsiveness, or the calculation of properties, the listening properties all use Watcher, and almost most of the features of Vue are inseparable from Watcher.
It even gives me a feeling that Vue is going big, that is, how to establish the relationship between data and Watcher, how to trigger Watcher updates when data changes, and how to update Watcher.value.
How this is updated determines what the Watcher is. If it is an updated view, it is a rendered Watcher, and if it is a computeWatcher, it is an updated calculation property.
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ exportdefaultclassWatcher { Vm: Component;//the vm instance where the Watcher is located Expression: string;//used to evaluate the value property of the Watcher, which can be a function or an expression Cb: Function;//callback function after each re-evaluation (value) Id: number;//Watcher id, unique identifier Deep: boolean;//whether to detect changes in depth user: boolean; Lazy: boolean;//Whether to perform a job search when the constructor function is executed, that is, to calculate the value value once through experssion sync: boolean; Dirty: boolean;//used with lazy active: boolean; Deps: Array < Dep >;//The dependencies of the current watcher are continuously increased by addDeps, but each time the value is recalculated, the dependencies that are not in newDepIds will be unregistered newDeps: Array < Dep >;//increase each time addDeps, recalculate and finally clear depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; Getter: Function;//convert experssion to function Value: any;//The value of the current watcher, for computed, is the calculation result, for rendered watcher, it is the rendering result
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm //This code shows that the _watcher on each vue instance is actually the rendering watcher if (isRenderWatcher) { vm._watcher = this } //The _watchers on the vue instance is all the watchers of the current instance, the first one is generally the rendering watcher vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy// for lazy watchers this.deps = [] this.newDeps = [] this.depIds = newSet() this.newDepIds = newSet() this.expression = process.env.NODE_ENV ! 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn = 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV ! 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } //This step is to determine whether it is lazy or not. If it is, it will not be evaluated for the time being. The calculation property is true here, and the rendering watcher is false here. this.value = this.lazy ? undefined : this.get() }
/** * Evaluate the getter, and re-collect dependencies. */ 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 }
/** * Add a dependency to this directive. */ //Collect dependencies to newDeps and record the id. Each time the value of this newDeps is recalculated, it will be assigned to dep and then cleared addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
/** * Clean up for dependency collection. */ cleanupDeps () { //According to newDeps to determine whether you still need to subscribe to the original dep, if not, unsubscribe through removeSub let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } //clear newDeps after setting deps to newDeps let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
/** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } elseif (this.sync) { this.run() } else { queueWatcher(this) } }
/** * Scheduler job interface. * Will be called by the scheduler. */ //currently active state, run () { if (this.active) { //recalculate the value const value = this.get() //The newly calculated value is different from the old value (this judgment is mainly to avoid that the value dependent on the calculated attribute has changed but the calculation result has not changed) 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) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
/** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false }
/** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } }
/** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
Dep
In general, watcher and dep are used together, so the source code of dep released here can be verified with the watcher source code above
/** * A dep is an observable that can have multiple * directives subscribing to it. */ exportdefaultclassDep { statictarget: ?Watcher; id: number; subs: Array<Watcher>;
depend () { if (Dep.target) { Dep.target.addDep(this) } }
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() } } }
// The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null const targetStack = []
Principles of Computational Attributes (computeWatcher)
initState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
exportfunctioninitState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData (vm)//initialize data } else { observe(vm._data = {}, true/* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch ! nativeWatch) { initWatch(vm, opts.watch) } }
It can be seen from this code that the reactive processing’observe (vm._data = {}, true/* asRootData */) 'is performed before initComputed, because the calculation property is also used by watcher, and it is also necessary to define reactive data for dependency collection.
In this way, it seems that the dependency collection of the calculated property is a simplified version of the response. The dependency collection in the principle of response is to call the getter of the data when rendering, and the calculated property is that the function itself calls the getter of the dependency number.
functioninitComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering()
for (const key in computed) { const userDef = computed[key] const getter = typeof userDef = 'function' ? userDef : userDef.get if (process.env.NODE_ENV ! 'production' && getter null) { warn( `Getter is missing for computed property "${key}".`, vm ) }
if (!isSSR) { // create internal watcher for the computed property. watchers[key] = newWatcher( vm, getter || noop, noop, computedWatcherOptions ) }
// component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. if (!(key in vm)) { defineComputed(vm, key, userDef) } elseif (process.env.NODE_ENV ! 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } elseif (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } }
exportfunctiondefineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef = 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache ! false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } if (process.env.NODE_ENV ! 'production' && sharedPropertyDefinition.set = noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
functioncreateComputedGetter (key) { returnfunctioncomputedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
This code is very simple. In fact, first define a watcher for each calculation property. Since’lazy 'is true, it will not be evaluated in the constructor function.
In defineComputed, the calculation property is mounted on the vm, and its getter returns the value of the watcher.
If dirty is true, you need to call’watcher.evaluate () ‘to recalculate watcher.value,’ dirty 'will be set to true after each update, and will be set to false after recalculation, and this calculation process will call the get method back, will first putshTarget, and then execute the function that calculates the properties, so that computedWatcher will subscribe to the changes in the data used in the function.
At the same time, its getter will also collect dependencies. When the Dep.target is not empty, it means that there is currently a rendering watcher under construction, so it is necessary to add its own watcher to the watchers of the current vue. In fact, this step is not to let the rendering watcher Subscribe to the changes of the computeWatcher, but to subscribe to the changes of the data that the computeWatcher relies on.
Listening attribute (userWatcher)
The process of userWatcher is relatively simple compared to the above two. After understanding the above two, the last one is easy to understand
This code can be seen that it will set the user of this watcher to true, which means that this watcher is userWatcher.
If immediate is true, the callback function will be executed immediately, which is the function we defined in the watcher.
The next logic is to create a new watcher, here we go back to the definition of watcher.
One thing to note is that when we define watcher, we usually use Attribute - Value Pair. In this case, expOrFn is the key name. At this time, Watcher will use’paresPath 'to find the corresponding data according to the path. At this time, watcher.getter is actually a function that returns the corresponding data.