observer.js 9.6 KB

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