| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- 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
- */
- function traverse (val, walkedObjs) {
- var i, keys
- walkedObjs = walkedObjs || {}
- if (isArray(val)) {
- i = val.length
- while (i--) traverse(val[i], walkedObjs)
- } else if (isObject(val)) {
- if (val.__ob__) {
- var depId = val.__ob__.dep.id
- if (walkedObjs[depId]) {
- return
- } else {
- walkedObjs[depId] = true
- }
- }
- keys = Object.keys(val)
- i = keys.length
- while (i--) traverse(val[keys[i]], walkedObjs)
- }
- }
|