watcher.js 5.1 KB

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