observer.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /* jshint proto:true */
  2. var Emitter = require('./emitter'),
  3. utils = require('./utils'),
  4. depsOb = require('./deps-parser').observer,
  5. // cache methods
  6. typeOf = utils.typeOf,
  7. def = utils.defProtected,
  8. slice = Array.prototype.slice,
  9. // Array mutation methods to wrap
  10. methods = ['push','pop','shift','unshift','splice','sort','reverse'],
  11. // fix for IE + __proto__ problem
  12. // define methods as inenumerable if __proto__ is present,
  13. // otherwise enumerable so we can loop through and manually
  14. // attach to array instances
  15. hasProto = ({}).__proto__,
  16. // lazy load
  17. ViewModel
  18. // The proxy prototype to replace the __proto__ of
  19. // an observed array
  20. var ArrayProxy = Object.create(Array.prototype)
  21. // Define mutation interceptors so we can emit the mutation info
  22. methods.forEach(function (method) {
  23. def(ArrayProxy, method, function () {
  24. var result = Array.prototype[method].apply(this, arguments)
  25. this.__observer__.emit('mutate', this.__observer__.path, this, {
  26. method: method,
  27. args: slice.call(arguments),
  28. result: result
  29. })
  30. return result
  31. }, !hasProto)
  32. })
  33. // Augment it with several convenience methods
  34. var extensions = {
  35. remove: function (index) {
  36. if (typeof index === 'function') {
  37. var i = this.length,
  38. removed = []
  39. while (i--) {
  40. if (index(this[i])) {
  41. removed.push(this.splice(i, 1)[0])
  42. }
  43. }
  44. return removed.reverse()
  45. } else {
  46. if (typeof index !== 'number') {
  47. index = this.indexOf(index)
  48. }
  49. if (index > -1) {
  50. return this.splice(index, 1)[0]
  51. }
  52. }
  53. },
  54. replace: function (index, data) {
  55. if (typeof index === 'function') {
  56. var i = this.length,
  57. replaced = [],
  58. replacer
  59. while (i--) {
  60. replacer = index(this[i])
  61. if (replacer !== undefined) {
  62. replaced.push(this.splice(i, 1, replacer)[0])
  63. }
  64. }
  65. return replaced.reverse()
  66. } else {
  67. if (typeof index !== 'number') {
  68. index = this.indexOf(index)
  69. }
  70. if (index > -1) {
  71. return this.splice(index, 1, data)[0]
  72. }
  73. }
  74. }
  75. }
  76. for (var method in extensions) {
  77. def(ArrayProxy, method, extensions[method], !hasProto)
  78. }
  79. /**
  80. * Watch an object based on type
  81. */
  82. function watch (obj, path, observer) {
  83. var type = typeOf(obj)
  84. if (type === 'Object') {
  85. watchObject(obj, path, observer)
  86. } else if (type === 'Array') {
  87. watchArray(obj, path, observer)
  88. }
  89. }
  90. /**
  91. * Watch an Object, recursive.
  92. */
  93. function watchObject (obj, path, observer) {
  94. for (var key in obj) {
  95. var keyPrefix = key.charAt(0)
  96. if (keyPrefix !== '$' && keyPrefix !== '_') {
  97. bind(obj, key, path, observer)
  98. }
  99. }
  100. }
  101. /**
  102. * Watch an Array, overload mutation methods
  103. * and add augmentations by intercepting the prototype chain
  104. */
  105. function watchArray (arr, path, observer) {
  106. def(arr, '__observer__', observer)
  107. observer.path = path
  108. if (hasProto) {
  109. arr.__proto__ = ArrayProxy
  110. } else {
  111. for (var key in ArrayProxy) {
  112. def(arr, key, ArrayProxy[key])
  113. }
  114. }
  115. }
  116. /**
  117. * Define accessors for a property on an Object
  118. * so it emits get/set events.
  119. * Then watch the value itself.
  120. */
  121. function bind (obj, key, path, observer) {
  122. var val = obj[key],
  123. watchable = isWatchable(val),
  124. values = observer.values,
  125. fullKey = (path ? path + '.' : '') + key
  126. values[fullKey] = val
  127. // emit set on bind
  128. // this means when an object is observed it will emit
  129. // a first batch of set events.
  130. observer.emit('set', fullKey, val)
  131. Object.defineProperty(obj, key, {
  132. enumerable: true,
  133. get: function () {
  134. // only emit get on tip values
  135. if (depsOb.active && !watchable) {
  136. observer.emit('get', fullKey)
  137. }
  138. return values[fullKey]
  139. },
  140. set: function (newVal) {
  141. values[fullKey] = newVal
  142. ensurePaths(key, newVal, values)
  143. observer.emit('set', fullKey, newVal)
  144. watch(newVal, fullKey, observer)
  145. }
  146. })
  147. watch(val, fullKey, observer)
  148. }
  149. /**
  150. * Check if a value is watchable
  151. */
  152. function isWatchable (obj) {
  153. ViewModel = ViewModel || require('./viewmodel')
  154. var type = typeOf(obj)
  155. return (type === 'Object' || type === 'Array') && !(obj instanceof ViewModel)
  156. }
  157. /**
  158. * When a value that is already converted is
  159. * observed again by another observer, we can skip
  160. * the watch conversion and simply emit set event for
  161. * all of its properties.
  162. */
  163. function emitSet (obj, observer, set) {
  164. if (typeOf(obj) === 'Array') {
  165. set('length', obj.length)
  166. } else {
  167. var key, val, values = observer.values
  168. for (key in observer.values) {
  169. val = values[key]
  170. set(key, val)
  171. }
  172. }
  173. }
  174. /**
  175. * Sometimes when a binding is found in the template, the value might
  176. * have not been set on the VM yet. To ensure computed properties and
  177. * dependency extraction can work, we have to create a dummy value for
  178. * any given path.
  179. */
  180. function ensurePaths (key, val, paths) {
  181. key += '.'
  182. for (var path in paths) {
  183. if (!path.indexOf(key)) {
  184. ensurePath(val, path.replace(key, ''))
  185. }
  186. }
  187. }
  188. /**
  189. * walk along a path and make sure it can be accessed
  190. * and enumerated in that object
  191. */
  192. function ensurePath (obj, key) {
  193. if (typeOf(obj) !== 'Object') return
  194. var path = key.split('.'), sec
  195. for (var i = 0, d = path.length - 1; i < d; i++) {
  196. sec = path[i]
  197. if (!obj[sec]) obj[sec] = {}
  198. obj = obj[sec]
  199. }
  200. var type = typeOf(obj)
  201. if (type === 'Object' || type === 'Array') {
  202. sec = path[i]
  203. if (!(sec in obj)) obj[sec] = undefined
  204. }
  205. return obj[sec]
  206. }
  207. module.exports = {
  208. // used in v-repeat
  209. watchArray: watchArray,
  210. ensurePath: ensurePath,
  211. ensurePaths: ensurePaths,
  212. /**
  213. * Observe an object with a given path,
  214. * and proxy get/set/mutate events to the provided observer.
  215. */
  216. observe: function (obj, rawPath, observer) {
  217. if (isWatchable(obj)) {
  218. var path = rawPath + '.',
  219. ob, alreadyConverted = !!obj.__observer__
  220. if (!alreadyConverted) {
  221. def(obj, '__observer__', new Emitter())
  222. }
  223. ob = obj.__observer__
  224. ob.values = ob.values || utils.hash()
  225. var proxies = observer.proxies[path] = {
  226. get: function (key) {
  227. observer.emit('get', path + key)
  228. },
  229. set: function (key, val) {
  230. observer.emit('set', path + key, val)
  231. },
  232. mutate: function (key, val, mutation) {
  233. // if the Array is a root value
  234. // the key will be null
  235. var fixedPath = key ? path + key : rawPath
  236. observer.emit('mutate', fixedPath, val, mutation)
  237. // also emit set for Array's length when it mutates
  238. var m = mutation.method
  239. if (m !== 'sort' && m !== 'reverse') {
  240. observer.emit('set', fixedPath + '.length', val.length)
  241. }
  242. }
  243. }
  244. ob
  245. .on('get', proxies.get)
  246. .on('set', proxies.set)
  247. .on('mutate', proxies.mutate)
  248. if (alreadyConverted) {
  249. emitSet(obj, ob, proxies.set)
  250. } else {
  251. watch(obj, null, ob)
  252. }
  253. }
  254. },
  255. /**
  256. * Cancel observation, turn off the listeners.
  257. */
  258. unobserve: function (obj, path, observer) {
  259. if (!obj || !obj.__observer__) return
  260. path = path + '.'
  261. var proxies = observer.proxies[path]
  262. obj.__observer__
  263. .off('get', proxies.get)
  264. .off('set', proxies.set)
  265. .off('mutate', proxies.mutate)
  266. observer.proxies[path] = null
  267. }
  268. }