watcher.js 7.9 KB

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