import {
|
warn,
|
remove,
|
isObject,
|
parsePath,
|
_Set as Set,
|
handleError,
|
invokeWithErrorHandling,
|
noop,
|
isFunction
|
} from '../util/index'
|
|
import { traverse } from './traverse'
|
import { queueWatcher } from './scheduler'
|
import Dep, { pushTarget, popTarget, DepTarget } from './dep'
|
import { DebuggerEvent, DebuggerOptions } from 'v3/debug'
|
|
import type { SimpleSet } from '../util/index'
|
import type { Component } from 'types/component'
|
import { activeEffectScope, recordEffectScope } from 'v3/reactivity/effectScope'
|
|
let uid = 0
|
|
/**
|
* @internal
|
*/
|
export interface WatcherOptions extends DebuggerOptions {
|
deep?: boolean
|
user?: boolean
|
lazy?: boolean
|
sync?: boolean
|
before?: Function
|
}
|
|
/**
|
* 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.
|
* @internal
|
*/
|
export default class Watcher implements DepTarget {
|
vm?: Component | null
|
expression: string
|
cb: Function
|
id: number
|
deep: boolean
|
user: boolean
|
lazy: boolean
|
sync: boolean
|
dirty: boolean
|
active: boolean
|
deps: Array<Dep>
|
newDeps: Array<Dep>
|
depIds: SimpleSet
|
newDepIds: SimpleSet
|
before?: Function
|
onStop?: Function
|
noRecurse?: boolean
|
getter: Function
|
value: any
|
post: boolean
|
|
// dev only
|
onTrack?: ((event: DebuggerEvent) => void) | undefined
|
onTrigger?: ((event: DebuggerEvent) => void) | undefined
|
|
constructor(
|
vm: Component | null,
|
expOrFn: string | (() => any),
|
cb: Function,
|
options?: WatcherOptions | null,
|
isRenderWatcher?: boolean
|
) {
|
recordEffectScope(
|
this,
|
// if the active effect scope is manually created (not a component scope),
|
// prioritize it
|
activeEffectScope && !activeEffectScope._vm
|
? activeEffectScope
|
: vm
|
? vm._scope
|
: undefined
|
)
|
if ((this.vm = vm) && isRenderWatcher) {
|
vm._watcher = this
|
}
|
// options
|
if (options) {
|
this.deep = !!options.deep
|
this.user = !!options.user
|
this.lazy = !!options.lazy
|
this.sync = !!options.sync
|
this.before = options.before
|
if (__DEV__) {
|
this.onTrack = options.onTrack
|
this.onTrigger = options.onTrigger
|
}
|
} else {
|
this.deep = this.user = this.lazy = this.sync = false
|
}
|
this.cb = cb
|
this.id = ++uid // uid for batching
|
this.active = true
|
this.post = false
|
this.dirty = this.lazy // for lazy watchers
|
this.deps = []
|
this.newDeps = []
|
this.depIds = new Set()
|
this.newDepIds = new Set()
|
this.expression = __DEV__ ? expOrFn.toString() : ''
|
// parse expression for getter
|
if (isFunction(expOrFn)) {
|
this.getter = expOrFn
|
} else {
|
this.getter = parsePath(expOrFn)
|
if (!this.getter) {
|
this.getter = noop
|
__DEV__ &&
|
warn(
|
`Failed watching path: "${expOrFn}" ` +
|
'Watcher only accepts simple dot-delimited paths. ' +
|
'For full control, use a function instead.',
|
vm
|
)
|
}
|
}
|
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: any) {
|
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.
|
*/
|
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() {
|
let i = this.deps.length
|
while (i--) {
|
const dep = this.deps[i]
|
if (!this.newDepIds.has(dep.id)) {
|
dep.removeSub(this)
|
}
|
}
|
let tmp: any = 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
|
} else if (this.sync) {
|
this.run()
|
} else {
|
queueWatcher(this)
|
}
|
}
|
|
/**
|
* Scheduler job interface.
|
* Will be called by the scheduler.
|
*/
|
run() {
|
if (this.active) {
|
const 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.
|
isObject(value) ||
|
this.deep
|
) {
|
// set new value
|
const oldValue = this.value
|
this.value = value
|
if (this.user) {
|
const info = `callback for watcher "${this.expression}"`
|
invokeWithErrorHandling(
|
this.cb,
|
this.vm,
|
[value, oldValue],
|
this.vm,
|
info
|
)
|
} 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.vm && !this.vm._isBeingDestroyed) {
|
remove(this.vm._scope.effects, this)
|
}
|
if (this.active) {
|
let i = this.deps.length
|
while (i--) {
|
this.deps[i].removeSub(this)
|
}
|
this.active = false
|
if (this.onStop) {
|
this.onStop()
|
}
|
}
|
}
|
}
|