UNPKG

5.41 kBJavaScriptView Raw
1/* @flow */
2
3import {
4 warn,
5 remove,
6 isObject,
7 parsePath,
8 _Set as Set,
9 handleError,
10 noop
11} from '../util/index'
12
13import { traverse } from './traverse'
14import { queueWatcher } from './scheduler'
15import Dep, { pushTarget, popTarget } from './dep'
16
17import type { SimpleSet } from '../util/index'
18
19let uid = 0
20
21/**
22 * A watcher parses an expression, collects dependencies,
23 * and fires callback when the expression value changes.
24 * This is used for both the $watch() api and directives.
25 */
26export default class Watcher {
27 vm: Component;
28 expression: string;
29 cb: Function;
30 id: number;
31 deep: boolean;
32 user: boolean;
33 lazy: boolean;
34 sync: boolean;
35 dirty: boolean;
36 active: boolean;
37 deps: Array<Dep>;
38 newDeps: Array<Dep>;
39 depIds: SimpleSet;
40 newDepIds: SimpleSet;
41 before: ?Function;
42 getter: Function;
43 value: any;
44
45 constructor (
46 vm: Component,
47 expOrFn: string | Function,
48 cb: Function,
49 options?: ?Object,
50 isRenderWatcher?: boolean
51 ) {
52 this.vm = vm
53 if (isRenderWatcher) {
54 vm._watcher = this
55 }
56 vm._watchers.push(this)
57 // options
58 if (options) {
59 this.deep = !!options.deep
60 this.user = !!options.user
61 this.lazy = !!options.lazy
62 this.sync = !!options.sync
63 this.before = options.before
64 } else {
65 this.deep = this.user = this.lazy = this.sync = false
66 }
67 this.cb = cb
68 this.id = ++uid // uid for batching
69 this.active = true
70 this.dirty = this.lazy // for lazy watchers
71 this.deps = []
72 this.newDeps = []
73 this.depIds = new Set()
74 this.newDepIds = new Set()
75 this.expression = process.env.NODE_ENV !== 'production'
76 ? expOrFn.toString()
77 : ''
78 // parse expression for getter
79 if (typeof expOrFn === 'function') {
80 this.getter = expOrFn
81 } else {
82 this.getter = parsePath(expOrFn)
83 if (!this.getter) {
84 this.getter = noop
85 process.env.NODE_ENV !== 'production' && warn(
86 `Failed watching path: "${expOrFn}" ` +
87 'Watcher only accepts simple dot-delimited paths. ' +
88 'For full control, use a function instead.',
89 vm
90 )
91 }
92 }
93 this.value = this.lazy
94 ? undefined
95 : this.get()
96 }
97
98 /**
99 * Evaluate the getter, and re-collect dependencies.
100 */
101 get () {
102 pushTarget(this)
103 let value
104 const vm = this.vm
105 try {
106 value = this.getter.call(vm, vm)
107 } catch (e) {
108 if (this.user) {
109 handleError(e, vm, `getter for watcher "${this.expression}"`)
110 } else {
111 throw e
112 }
113 } finally {
114 // "touch" every property so they are all tracked as
115 // dependencies for deep watching
116 if (this.deep) {
117 traverse(value)
118 }
119 popTarget()
120 this.cleanupDeps()
121 }
122 return value
123 }
124
125 /**
126 * Add a dependency to this directive.
127 */
128 addDep (dep: Dep) {
129 const id = dep.id
130 if (!this.newDepIds.has(id)) {
131 this.newDepIds.add(id)
132 this.newDeps.push(dep)
133 if (!this.depIds.has(id)) {
134 dep.addSub(this)
135 }
136 }
137 }
138
139 /**
140 * Clean up for dependency collection.
141 */
142 cleanupDeps () {
143 let i = this.deps.length
144 while (i--) {
145 const dep = this.deps[i]
146 if (!this.newDepIds.has(dep.id)) {
147 dep.removeSub(this)
148 }
149 }
150 let tmp = this.depIds
151 this.depIds = this.newDepIds
152 this.newDepIds = tmp
153 this.newDepIds.clear()
154 tmp = this.deps
155 this.deps = this.newDeps
156 this.newDeps = tmp
157 this.newDeps.length = 0
158 }
159
160 /**
161 * Subscriber interface.
162 * Will be called when a dependency changes.
163 */
164 update () {
165 /* istanbul ignore else */
166 if (this.lazy) {
167 this.dirty = true
168 } else if (this.sync) {
169 this.run()
170 } else {
171 queueWatcher(this)
172 }
173 }
174
175 /**
176 * Scheduler job interface.
177 * Will be called by the scheduler.
178 */
179 run () {
180 if (this.active) {
181 const value = this.get()
182 if (
183 value !== this.value ||
184 // Deep watchers and watchers on Object/Arrays should fire even
185 // when the value is the same, because the value may
186 // have mutated.
187 isObject(value) ||
188 this.deep
189 ) {
190 // set new value
191 const oldValue = this.value
192 this.value = value
193 if (this.user) {
194 try {
195 this.cb.call(this.vm, value, oldValue)
196 } catch (e) {
197 handleError(e, this.vm, `callback for watcher "${this.expression}"`)
198 }
199 } else {
200 this.cb.call(this.vm, value, oldValue)
201 }
202 }
203 }
204 }
205
206 /**
207 * Evaluate the value of the watcher.
208 * This only gets called for lazy watchers.
209 */
210 evaluate () {
211 this.value = this.get()
212 this.dirty = false
213 }
214
215 /**
216 * Depend on all deps collected by this watcher.
217 */
218 depend () {
219 let i = this.deps.length
220 while (i--) {
221 this.deps[i].depend()
222 }
223 }
224
225 /**
226 * Remove self from all dependencies' subscriber list.
227 */
228 teardown () {
229 if (this.active) {
230 // remove self from vm's watcher list
231 // this is a somewhat expensive operation so we skip it
232 // if the vm is being destroyed.
233 if (!this.vm._isBeingDestroyed) {
234 remove(this.vm._watchers, this)
235 }
236 let i = this.deps.length
237 while (i--) {
238 this.deps[i].removeSub(this)
239 }
240 this.active = false
241 }
242 }
243}