import config from '../config' import Dep from './dep' import { pushWatcher } from './batcher' import { extend, isArray, isObject, _Set as Set } from '../util/index' let uid = 0 /** * 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. * * @param {Vue} vm * @param {String|Function} expOrFn * @param {Function} cb * @param {Object} options * - {Array} filters * - {Boolean} twoWay * - {Boolean} deep * - {Boolean} user * - {Boolean} sync * - {Boolean} lazy * - {Function} [preProcess] * - {Function} [postProcess] * @constructor */ export default function Watcher (vm, expOrFn, cb, options) { // mix in options if (options) { extend(this, options) } var isFn = typeof expOrFn === 'function' this.vm = vm vm._watchers.push(this) this.expression = expOrFn 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 = new Set() this.newDepIds = new Set() // parse expression for getter if (isFn) { this.getter = expOrFn } else { this.getter = new Function(`with(this){return ${expOrFn}}`) } this.value = this.lazy ? undefined : this.get() // state for avoiding false triggers for deep and Array // watchers during vm._digest() this.queued = this.shallow = false } /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function () { this.beforeGet() const value = this.getter.call(this.vm, this.vm) // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } this.afterGet() return value } /** * Prepare for dependency collection. */ Watcher.prototype.beforeGet = function () { Dep.target = this } /** * Add a dependency to this directive. * * @param {Dep} dep */ Watcher.prototype.addDep = function (dep) { var 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. */ Watcher.prototype.afterGet = function () { Dep.target = null var i = this.deps.length while (i--) { var dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } var 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. * * @param {Boolean} shallow */ Watcher.prototype.update = function (shallow) { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { // if queued, only overwrite shallow with non-shallow, // but not the other way around. this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow this.queued = true // record before-push error stack in debug mode /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.debug) { this.prevError = new Error('[vue] async stack trace') } pushWatcher(this) } } /** * Batcher job interface. * Will be called by the batcher. */ Watcher.prototype.run = function () { if (this.active) { var 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; but only do so if this is a // non-shallow update (caused by a vm digest). ((isObject(value) || this.deep) && !this.shallow) ) { // set new value var oldValue = this.value this.value = value this.cb.call(this.vm, value, oldValue) } this.queued = this.shallow = false } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ Watcher.prototype.evaluate = function () { // avoid overwriting another watcher that is being // collected. var current = Dep.target this.value = this.get() this.dirty = false Dep.target = current } /** * Depend on all deps collected by this watcher. */ Watcher.prototype.depend = function () { var i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subcriber list. */ Watcher.prototype.teardown = function () { 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 or is performing a v-for // re-render (the watcher list is then filtered by v-for). if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) { this.vm._watchers.$remove(this) } var i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false this.vm = this.cb = this.value = null } } /** * Recrusively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. * * @param {*} val */ function traverse (val) { var i, keys if (isArray(val)) { i = val.length while (i--) traverse(val[i]) } else if (isObject(val)) { keys = Object.keys(val) i = keys.length while (i--) traverse(val[keys[i]]) } }