var _ = require('./util') var config = require('./config') var Dep = require('./observer/dep') var expParser = require('./parsers/expression') var batcher = require('./batcher') var 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} expression * @param {Function} cb * @param {Object} options * - {Array} filters * - {Boolean} twoWay * - {Boolean} deep * - {Boolean} user * - {Boolean} sync * - {Boolean} lazy * - {Function} [preProcess] * @constructor */ 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 = isFn ? expOrFn.toString() : expOrFn this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = Object.create(null) this.newDeps = null this.prevError = null // for async error stacks // parse expression for getter/setter if (isFn) { this.getter = expOrFn this.setter = undefined } else { var res = expParser.parse(expOrFn, this.twoWay) this.getter = res.get this.setter = res.set } 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 } /** * Add a dependency to this directive. * * @param {Dep} dep */ Watcher.prototype.addDep = function (dep) { var id = dep.id if (!this.newDeps[id]) { this.newDeps[id] = dep if (!this.deps[id]) { this.deps[id] = dep dep.addSub(this) } } } /** * Evaluate the getter, and re-collect dependencies. */ Watcher.prototype.get = function () { this.beforeGet() var vm = this.vm var value try { value = this.getter.call(vm, vm) } catch (e) { if ( process.env.NODE_ENV !== 'production' && config.warnExpressionErrors ) { _.warn( 'Error when evaluating expression "' + this.expression + '". ' + (config.debug ? '' : 'Turn on debug mode to see stack trace.' ), e ) } } // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } if (this.preProcess) { value = this.preProcess(value) } if (this.filters) { value = vm._applyFilters(value, null, this.filters, false) } this.afterGet() return value } /** * Set the corresponding value with the setter. * * @param {*} value */ Watcher.prototype.set = function (value) { var vm = this.vm if (this.filters) { value = vm._applyFilters( value, this.value, this.filters, true) } try { this.setter.call(vm, vm, value) } catch (e) { if ( process.env.NODE_ENV !== 'production' && config.warnExpressionErrors ) { _.warn( 'Error when evaluating setter "' + this.expression + '"', e ) } } } /** * Prepare for dependency collection. */ Watcher.prototype.beforeGet = function () { Dep.target = this this.newDeps = Object.create(null) } /** * Clean up for dependency collection. */ Watcher.prototype.afterGet = function () { Dep.target = null var ids = Object.keys(this.deps) var i = ids.length while (i--) { var id = ids[i] if (!this.newDeps[id]) { this.deps[id].removeSub(this) } } this.deps = this.newDeps } /** * 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 || !config.async) { 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') } batcher.push(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 Array watchers 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). ((_.isArray(value) || this.deep) && !this.shallow) ) { // set new value var oldValue = this.value this.value = value // in debug + async mode, when a watcher callbacks // throws, we also throw the saved before-push error // so the full cross-tick stack trace is available. var prevError = this.prevError /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.debug && prevError) { this.prevError = null try { this.cb.call(this.vm, value, oldValue) } catch (e) { _.nextTick(function () { throw prevError }, 0) throw e } } else { 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 depIds = Object.keys(this.deps) var i = depIds.length while (i--) { this.deps[depIds[i]].depend() } } /** * Remove self from all dependencies' subcriber list. */ Watcher.prototype.teardown = function () { if (this.active) { // remove self from vm's watcher list // we can skip this if the vm if being destroyed // which can improve teardown performance. if (!this.vm._isBeingDestroyed) { this.vm._watchers.$remove(this) } var depIds = Object.keys(this.deps) var i = depIds.length while (i--) { this.deps[depIds[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 {Object} obj */ function traverse (obj) { var key, val, i for (key in obj) { val = obj[key] if (_.isArray(val)) { i = val.length while (i--) traverse(val[i]) } else if (_.isObject(val)) { traverse(val) } } } module.exports = Watcher