watcher.js 7.7 KB

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