watcher.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import config from './config'
  2. import Dep from './observer/dep'
  3. import { parseExpression } from './parsers/expression'
  4. import { pushWatcher } from './batcher'
  5. import {
  6. extend,
  7. warn,
  8. isArray,
  9. isObject,
  10. nextTick,
  11. _Set as Set
  12. } from './util/index'
  13. let uid = 0
  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. this.prevError = null // for async error stacks
  51. // parse expression for getter/setter
  52. if (isFn) {
  53. this.getter = expOrFn
  54. this.setter = undefined
  55. } else {
  56. var res = parseExpression(expOrFn, this.twoWay)
  57. this.getter = res.get
  58. this.setter = res.set
  59. }
  60. this.value = this.lazy
  61. ? undefined
  62. : this.get()
  63. // state for avoiding false triggers for deep and Array
  64. // watchers during vm._digest()
  65. this.queued = this.shallow = false
  66. }
  67. /**
  68. * Evaluate the getter, and re-collect dependencies.
  69. */
  70. Watcher.prototype.get = function () {
  71. this.beforeGet()
  72. var scope = this.scope || this.vm
  73. var value
  74. try {
  75. value = this.getter.call(scope, scope)
  76. } catch (e) {
  77. if (
  78. process.env.NODE_ENV !== 'production' &&
  79. config.warnExpressionErrors
  80. ) {
  81. warn(
  82. 'Error when evaluating expression ' +
  83. '"' + this.expression + '": ' + e.toString(),
  84. this.vm
  85. )
  86. }
  87. }
  88. // "touch" every property so they are all tracked as
  89. // dependencies for deep watching
  90. if (this.deep) {
  91. traverse(value)
  92. }
  93. if (this.preProcess) {
  94. value = this.preProcess(value)
  95. }
  96. if (this.filters) {
  97. value = scope._applyFilters(value, null, this.filters, false)
  98. }
  99. if (this.postProcess) {
  100. value = this.postProcess(value)
  101. }
  102. this.afterGet()
  103. return value
  104. }
  105. /**
  106. * Set the corresponding value with the setter.
  107. *
  108. * @param {*} value
  109. */
  110. Watcher.prototype.set = function (value) {
  111. var scope = this.scope || this.vm
  112. if (this.filters) {
  113. value = scope._applyFilters(
  114. value, this.value, this.filters, true)
  115. }
  116. try {
  117. this.setter.call(scope, scope, value)
  118. } catch (e) {
  119. if (
  120. process.env.NODE_ENV !== 'production' &&
  121. config.warnExpressionErrors
  122. ) {
  123. warn(
  124. 'Error when evaluating setter ' +
  125. '"' + this.expression + '": ' + e.toString(),
  126. this.vm
  127. )
  128. }
  129. }
  130. // two-way sync for v-for alias
  131. var forContext = scope.$forContext
  132. if (forContext && forContext.alias === this.expression) {
  133. if (forContext.filters) {
  134. process.env.NODE_ENV !== 'production' && warn(
  135. 'It seems you are using two-way binding on ' +
  136. 'a v-for alias (' + this.expression + '), and the ' +
  137. 'v-for has filters. This will not work properly. ' +
  138. 'Either remove the filters or use an array of ' +
  139. 'objects and bind to object properties instead.',
  140. this.vm
  141. )
  142. return
  143. }
  144. forContext._withLock(function () {
  145. if (scope.$key) { // original is an object
  146. forContext.rawValue[scope.$key] = value
  147. } else {
  148. forContext.rawValue.$set(scope.$index, value)
  149. }
  150. })
  151. }
  152. }
  153. /**
  154. * Prepare for dependency collection.
  155. */
  156. Watcher.prototype.beforeGet = function () {
  157. Dep.target = this
  158. }
  159. /**
  160. * Add a dependency to this directive.
  161. *
  162. * @param {Dep} dep
  163. */
  164. Watcher.prototype.addDep = function (dep) {
  165. var id = dep.id
  166. if (!this.newDepIds.has(id)) {
  167. this.newDepIds.add(id)
  168. this.newDeps.push(dep)
  169. if (!this.depIds.has(id)) {
  170. dep.addSub(this)
  171. }
  172. }
  173. }
  174. /**
  175. * Clean up for dependency collection.
  176. */
  177. Watcher.prototype.afterGet = function () {
  178. Dep.target = null
  179. var i = this.deps.length
  180. while (i--) {
  181. var dep = this.deps[i]
  182. if (!this.newDepIds.has(dep.id)) {
  183. dep.removeSub(this)
  184. }
  185. }
  186. var tmp = this.depIds
  187. this.depIds = this.newDepIds
  188. this.newDepIds = tmp
  189. this.newDepIds.clear()
  190. tmp = this.deps
  191. this.deps = this.newDeps
  192. this.newDeps = tmp
  193. this.newDeps.length = 0
  194. }
  195. /**
  196. * Subscriber interface.
  197. * Will be called when a dependency changes.
  198. *
  199. * @param {Boolean} shallow
  200. */
  201. Watcher.prototype.update = function (shallow) {
  202. if (this.lazy) {
  203. this.dirty = true
  204. } else if (this.sync || !config.async) {
  205. this.run()
  206. } else {
  207. // if queued, only overwrite shallow with non-shallow,
  208. // but not the other way around.
  209. this.shallow = this.queued
  210. ? shallow
  211. ? this.shallow
  212. : false
  213. : !!shallow
  214. this.queued = true
  215. // record before-push error stack in debug mode
  216. /* istanbul ignore if */
  217. if (process.env.NODE_ENV !== 'production' && config.debug) {
  218. this.prevError = new Error('[vue] async stack trace')
  219. }
  220. pushWatcher(this)
  221. }
  222. }
  223. /**
  224. * Batcher job interface.
  225. * Will be called by the batcher.
  226. */
  227. Watcher.prototype.run = function () {
  228. if (this.active) {
  229. var value = this.get()
  230. if (
  231. value !== this.value ||
  232. // Deep watchers and watchers on Object/Arrays should fire even
  233. // when the value is the same, because the value may
  234. // have mutated; but only do so if this is a
  235. // non-shallow update (caused by a vm digest).
  236. ((isObject(value) || this.deep) && !this.shallow)
  237. ) {
  238. // set new value
  239. var oldValue = this.value
  240. this.value = value
  241. // in debug + async mode, when a watcher callbacks
  242. // throws, we also throw the saved before-push error
  243. // so the full cross-tick stack trace is available.
  244. var prevError = this.prevError
  245. /* istanbul ignore if */
  246. if (process.env.NODE_ENV !== 'production' &&
  247. config.debug && prevError) {
  248. this.prevError = null
  249. try {
  250. this.cb.call(this.vm, value, oldValue)
  251. } catch (e) {
  252. nextTick(function () {
  253. throw prevError
  254. }, 0)
  255. throw e
  256. }
  257. } else {
  258. this.cb.call(this.vm, value, oldValue)
  259. }
  260. }
  261. this.queued = this.shallow = false
  262. }
  263. }
  264. /**
  265. * Evaluate the value of the watcher.
  266. * This only gets called for lazy watchers.
  267. */
  268. Watcher.prototype.evaluate = function () {
  269. // avoid overwriting another watcher that is being
  270. // collected.
  271. var current = Dep.target
  272. this.value = this.get()
  273. this.dirty = false
  274. Dep.target = current
  275. }
  276. /**
  277. * Depend on all deps collected by this watcher.
  278. */
  279. Watcher.prototype.depend = function () {
  280. var i = this.deps.length
  281. while (i--) {
  282. this.deps[i].depend()
  283. }
  284. }
  285. /**
  286. * Remove self from all dependencies' subcriber list.
  287. */
  288. Watcher.prototype.teardown = function () {
  289. if (this.active) {
  290. // remove self from vm's watcher list
  291. // this is a somewhat expensive operation so we skip it
  292. // if the vm is being destroyed or is performing a v-for
  293. // re-render (the watcher list is then filtered by v-for).
  294. if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
  295. this.vm._watchers.$remove(this)
  296. }
  297. var i = this.deps.length
  298. while (i--) {
  299. this.deps[i].removeSub(this)
  300. }
  301. this.active = false
  302. this.vm = this.cb = this.value = null
  303. }
  304. }
  305. /**
  306. * Recrusively traverse an object to evoke all converted
  307. * getters, so that every nested property inside the object
  308. * is collected as a "deep" dependency.
  309. *
  310. * @param {*} val
  311. */
  312. function traverse (val, walkedObjs) {
  313. var i, keys
  314. walkedObjs = walkedObjs || {}
  315. if (isArray(val)) {
  316. i = val.length
  317. while (i--) traverse(val[i], walkedObjs)
  318. } else if (isObject(val)) {
  319. if (val.__ob__) {
  320. var depId = val.__ob__.dep.id
  321. if (walkedObjs[depId]) {
  322. return
  323. } else {
  324. walkedObjs[depId] = true
  325. }
  326. }
  327. keys = Object.keys(val)
  328. i = keys.length
  329. while (i--) traverse(val[keys[i]], walkedObjs)
  330. }
  331. }