watcher.js 5.6 KB

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