import config from './config'
import Dep from './observer/dep'
import { parseExpression } from './parsers/expression'
import { pushWatcher } from './batcher'
import {
extend,
warn,
isArray,
isObject,
nextTick,
_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()
this.prevError = null // for async error stacks
// parse expression for getter/setter
if (isFn) {
this.getter = expOrFn
this.setter = undefined
} else {
var res = parseExpression(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
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function () {
this.beforeGet()
var scope = this.scope || this.vm
var value
try {
value = this.getter.call(scope, scope)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating expression ' +
'"' + this.expression + '": ' + e.toString(),
this.vm
)
}
}
// "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 = scope._applyFilters(value, null, this.filters, false)
}
if (this.postProcess) {
value = this.postProcess(value)
}
this.afterGet()
return value
}
/**
* Set the corresponding value with the setter.
*
* @param {*} value
*/
Watcher.prototype.set = function (value) {
var scope = this.scope || this.vm
if (this.filters) {
value = scope._applyFilters(
value, this.value, this.filters, true)
}
try {
this.setter.call(scope, scope, value)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating setter ' +
'"' + this.expression + '": ' + e.toString(),
this.vm
)
}
}
// two-way sync for v-for alias
var forContext = scope.$forContext
if (forContext && forContext.alias === this.expression) {
if (forContext.filters) {
process.env.NODE_ENV !== 'production' && warn(
'It seems you are using two-way binding on ' +
'a v-for alias (' + this.expression + '), and the ' +
'v-for has filters. This will not work properly. ' +
'Either remove the filters or use an array of ' +
'objects and bind to object properties instead.',
this.vm
)
return
}
forContext._withLock(function () {
if (scope.$key) { // original is an object
forContext.rawValue[scope.$key] = value
} else {
forContext.rawValue.$set(scope.$index, 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 || !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')
}
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
// 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 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
*/
const seenObjects = new Set()
function traverse (val, seen) {
let i, keys
if (!seen) {
seen = seenObjects
seen.clear()
}
const isA = isArray(val)
const isO = isObject(val)
if ((isA || isO) && Object.isExtensible(val)) {
if (val.__ob__) {
var depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
} else {
seen.add(depId)
}
}
if (isA) {
i = val.length
while (i--) traverse(val[i], seen)
} else if (isO) {
keys = Object.keys(val)
i = keys.length
while (i--) traverse(val[keys[i]], seen)
}
}
}
|