watcher.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. var _ = require('./util')
  2. var config = require('./config')
  3. var Observer = require('./observer')
  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. * @constructor
  21. */
  22. function Watcher (vm, expression, cb, options) {
  23. this.vm = vm
  24. vm._watcherList.push(this)
  25. this.expression = expression
  26. this.cbs = [cb]
  27. this.id = ++uid // uid for batching
  28. this.active = true
  29. options = options || {}
  30. this.deep = options.deep
  31. this.user = options.user
  32. this.deps = Object.create(null)
  33. // setup filters if any.
  34. // We delegate directive filters here to the watcher
  35. // because they need to be included in the dependency
  36. // collection process.
  37. if (options.filters) {
  38. this.readFilters = options.filters.read
  39. this.writeFilters = options.filters.write
  40. }
  41. // parse expression for getter/setter
  42. var res = expParser.parse(expression, options.twoWay)
  43. this.getter = res.get
  44. this.setter = res.set
  45. this.value = this.get()
  46. }
  47. var p = Watcher.prototype
  48. /**
  49. * Add a dependency to this directive.
  50. *
  51. * @param {Dep} dep
  52. */
  53. p.addDep = function (dep) {
  54. var id = dep.id
  55. if (!this.newDeps[id]) {
  56. this.newDeps[id] = dep
  57. if (!this.deps[id]) {
  58. this.deps[id] = dep
  59. dep.addSub(this)
  60. }
  61. }
  62. }
  63. /**
  64. * Evaluate the getter, and re-collect dependencies.
  65. */
  66. p.get = function () {
  67. this.beforeGet()
  68. var vm = this.vm
  69. var value
  70. try {
  71. value = this.getter.call(vm, vm)
  72. } catch (e) {
  73. _.warn(
  74. 'Error when evaluating expression "' +
  75. this.expression + '":\n ' + e
  76. )
  77. }
  78. // "touch" every property so they are all tracked as
  79. // dependencies for deep watching
  80. if (this.deep) {
  81. traverse(value)
  82. }
  83. value = _.applyFilters(value, this.readFilters, vm)
  84. this.afterGet()
  85. return value
  86. }
  87. /**
  88. * Set the corresponding value with the setter.
  89. *
  90. * @param {*} value
  91. */
  92. p.set = function (value) {
  93. var vm = this.vm
  94. value = _.applyFilters(
  95. value, this.writeFilters, vm, this.value
  96. )
  97. try {
  98. this.setter.call(vm, vm, value)
  99. } catch (e) {
  100. _.warn(
  101. 'Error when evaluating setter "' +
  102. this.expression + '":\n ' + e
  103. )
  104. }
  105. }
  106. /**
  107. * Prepare for dependency collection.
  108. */
  109. p.beforeGet = function () {
  110. Observer.target = this
  111. this.newDeps = {}
  112. }
  113. /**
  114. * Clean up for dependency collection.
  115. */
  116. p.afterGet = function () {
  117. Observer.target = null
  118. for (var id in this.deps) {
  119. if (!this.newDeps[id]) {
  120. this.deps[id].removeSub(this)
  121. }
  122. }
  123. this.deps = this.newDeps
  124. }
  125. /**
  126. * Subscriber interface.
  127. * Will be called when a dependency changes.
  128. */
  129. p.update = function () {
  130. if (!config.async || config.debug) {
  131. this.run()
  132. } else {
  133. batcher.push(this)
  134. }
  135. }
  136. /**
  137. * Batcher job interface.
  138. * Will be called by the batcher.
  139. */
  140. p.run = function () {
  141. if (this.active) {
  142. var value = this.get()
  143. if (
  144. (typeof value === 'object' && value !== null) ||
  145. value !== this.value
  146. ) {
  147. var oldValue = this.value
  148. this.value = value
  149. var cbs = this.cbs
  150. for (var i = 0, l = cbs.length; i < l; i++) {
  151. cbs[i](value, oldValue)
  152. // if a callback also removed other callbacks,
  153. // we need to adjust the loop accordingly.
  154. var removed = l - cbs.length
  155. if (removed) {
  156. i -= removed
  157. l -= removed
  158. }
  159. }
  160. }
  161. }
  162. }
  163. /**
  164. * Add a callback.
  165. *
  166. * @param {Function} cb
  167. */
  168. p.addCb = function (cb) {
  169. this.cbs.push(cb)
  170. }
  171. /**
  172. * Remove a callback.
  173. *
  174. * @param {Function} cb
  175. */
  176. p.removeCb = function (cb) {
  177. var cbs = this.cbs
  178. if (cbs.length > 1) {
  179. var i = cbs.indexOf(cb)
  180. if (i > -1) {
  181. cbs.splice(i, 1)
  182. }
  183. } else if (cb === cbs[0]) {
  184. this.teardown()
  185. }
  186. }
  187. /**
  188. * Remove self from all dependencies' subcriber list.
  189. */
  190. p.teardown = function () {
  191. if (this.active) {
  192. // remove self from vm's watcher list
  193. // we can skip this if the vm if being destroyed
  194. // which can improve teardown performance.
  195. if (!this.vm._isBeingDestroyed) {
  196. var list = this.vm._watcherList
  197. list.splice(list.indexOf(this))
  198. }
  199. for (var id in this.deps) {
  200. this.deps[id].removeSub(this)
  201. }
  202. this.active = false
  203. this.vm = this.cbs = this.value = null
  204. }
  205. }
  206. /**
  207. * Recrusively traverse an object to evoke all converted
  208. * getters, so that every nested property inside the object
  209. * is collected as a "deep" dependency.
  210. *
  211. * @param {Object} obj
  212. */
  213. function traverse (obj) {
  214. var key, val, i
  215. for (key in obj) {
  216. val = obj[key]
  217. if (_.isArray(val)) {
  218. i = val.length
  219. while (i--) traverse(val[i])
  220. } else if (_.isObject(val)) {
  221. traverse(val)
  222. }
  223. }
  224. }
  225. module.exports = Watcher