observer.js 8.5 KB

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