watcher.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import config from '../config'
  2. import Dep from './dep'
  3. import { pushWatcher } from './batcher'
  4. import {
  5. warn,
  6. extend,
  7. isArray,
  8. isObject,
  9. parsePath,
  10. _Set as Set
  11. } from '../util/index'
  12. let uid = 0
  13. let prevTarget
  14. /**
  15. * A watcher parses an expression, collects dependencies,
  16. * and fires callback when the expression value changes.
  17. * This is used for both the $watch() api and directives.
  18. *
  19. * @param {Vue} vm
  20. * @param {String|Function} expOrFn
  21. * @param {Function} cb
  22. * @param {Object} options
  23. * - {Array} filters
  24. * - {Boolean} twoWay
  25. * - {Boolean} deep
  26. * - {Boolean} user
  27. * - {Boolean} sync
  28. * - {Boolean} lazy
  29. * - {Function} [preProcess]
  30. * - {Function} [postProcess]
  31. * @constructor
  32. */
  33. export default function Watcher (vm, expOrFn, cb, options) {
  34. // mix in options
  35. if (options) {
  36. extend(this, options)
  37. }
  38. var isFn = typeof expOrFn === 'function'
  39. this.vm = vm
  40. vm._watchers.push(this)
  41. this.expression = expOrFn
  42. this.cb = cb
  43. this.id = ++uid // uid for batching
  44. this.active = true
  45. this.dirty = this.lazy // for lazy watchers
  46. this.deps = []
  47. this.newDeps = []
  48. this.depIds = new Set()
  49. this.newDepIds = new Set()
  50. // parse expression for getter
  51. if (isFn) {
  52. this.getter = expOrFn
  53. } else {
  54. this.getter = parsePath(expOrFn)
  55. if (!this.getter) {
  56. this.getter = function () {}
  57. process.env.NODE_ENV !== 'production' && warn(
  58. 'Failed watching path: ' + expOrFn +
  59. 'Watcher only accepts simple dot-delimited paths. ' +
  60. 'For full control, use a function instead.',
  61. vm
  62. )
  63. }
  64. }
  65. this.value = this.lazy
  66. ? undefined
  67. : this.get()
  68. // state for avoiding false triggers for deep and Array
  69. // watchers during vm._digest()
  70. this.queued = this.shallow = false
  71. }
  72. /**
  73. * Evaluate the getter, and re-collect dependencies.
  74. */
  75. Watcher.prototype.get = function () {
  76. this.beforeGet()
  77. const value = this.getter.call(this.vm, this.vm)
  78. // "touch" every property so they are all tracked as
  79. // dependencies for deep watching
  80. if (this.deep) {
  81. traverse(value)
  82. }
  83. this.afterGet()
  84. return value
  85. }
  86. /**
  87. * Prepare for dependency collection.
  88. */
  89. Watcher.prototype.beforeGet = function () {
  90. prevTarget = Dep.target
  91. Dep.target = this
  92. }
  93. /**
  94. * Add a dependency to this directive.
  95. *
  96. * @param {Dep} dep
  97. */
  98. Watcher.prototype.addDep = function (dep) {
  99. var id = dep.id
  100. if (!this.newDepIds.has(id)) {
  101. this.newDepIds.add(id)
  102. this.newDeps.push(dep)
  103. if (!this.depIds.has(id)) {
  104. dep.addSub(this)
  105. }
  106. }
  107. }
  108. /**
  109. * Clean up for dependency collection.
  110. */
  111. Watcher.prototype.afterGet = function () {
  112. Dep.target = prevTarget
  113. var i = this.deps.length
  114. while (i--) {
  115. var dep = this.deps[i]
  116. if (!this.newDepIds.has(dep.id)) {
  117. dep.removeSub(this)
  118. }
  119. }
  120. var tmp = this.depIds
  121. this.depIds = this.newDepIds
  122. this.newDepIds = tmp
  123. this.newDepIds.clear()
  124. tmp = this.deps
  125. this.deps = this.newDeps
  126. this.newDeps = tmp
  127. this.newDeps.length = 0
  128. }
  129. /**
  130. * Subscriber interface.
  131. * Will be called when a dependency changes.
  132. *
  133. * @param {Boolean} shallow
  134. */
  135. Watcher.prototype.update = function (shallow) {
  136. if (this.lazy) {
  137. this.dirty = true
  138. } else if (this.sync) {
  139. this.run()
  140. } else {
  141. // if queued, only overwrite shallow with non-shallow,
  142. // but not the other way around.
  143. this.shallow = this.queued
  144. ? shallow
  145. ? this.shallow
  146. : false
  147. : !!shallow
  148. this.queued = true
  149. // record before-push error stack in debug mode
  150. /* istanbul ignore if */
  151. if (process.env.NODE_ENV !== 'production' && config.debug) {
  152. this.prevError = new Error('[vue] async stack trace')
  153. }
  154. pushWatcher(this)
  155. }
  156. }
  157. /**
  158. * Batcher job interface.
  159. * Will be called by the batcher.
  160. */
  161. Watcher.prototype.run = function () {
  162. if (this.active) {
  163. var 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; but only do so if this is a
  169. // non-shallow update (caused by a vm digest).
  170. ((isObject(value) || this.deep) && !this.shallow)
  171. ) {
  172. // set new value
  173. var oldValue = this.value
  174. this.value = value
  175. this.cb.call(this.vm, value, oldValue)
  176. }
  177. this.queued = this.shallow = false
  178. }
  179. }
  180. /**
  181. * Evaluate the value of the watcher.
  182. * This only gets called for lazy watchers.
  183. */
  184. Watcher.prototype.evaluate = function () {
  185. // avoid overwriting another watcher that is being
  186. // collected.
  187. var current = Dep.target
  188. this.value = this.get()
  189. this.dirty = false
  190. Dep.target = current
  191. }
  192. /**
  193. * Depend on all deps collected by this watcher.
  194. */
  195. Watcher.prototype.depend = function () {
  196. var i = this.deps.length
  197. while (i--) {
  198. this.deps[i].depend()
  199. }
  200. }
  201. /**
  202. * Remove self from all dependencies' subcriber list.
  203. */
  204. Watcher.prototype.teardown = function () {
  205. if (this.active) {
  206. // remove self from vm's watcher list
  207. // this is a somewhat expensive operation so we skip it
  208. // if the vm is being destroyed or is performing a v-for
  209. // re-render (the watcher list is then filtered by v-for).
  210. if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
  211. this.vm._watchers.$remove(this)
  212. }
  213. var i = this.deps.length
  214. while (i--) {
  215. this.deps[i].removeSub(this)
  216. }
  217. this.active = false
  218. this.vm = this.cb = this.value = null
  219. }
  220. }
  221. /**
  222. * Recrusively traverse an object to evoke all converted
  223. * getters, so that every nested property inside the object
  224. * is collected as a "deep" dependency.
  225. *
  226. * @param {*} val
  227. */
  228. function traverse (val) {
  229. var i, keys
  230. if (isArray(val)) {
  231. i = val.length
  232. while (i--) traverse(val[i])
  233. } else if (isObject(val)) {
  234. keys = Object.keys(val)
  235. i = keys.length
  236. while (i--) traverse(val[keys[i]])
  237. }
  238. }