observer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. hasOwn = ({}).hasOwnProperty,
  8. oDef = Object.defineProperty,
  9. slice = [].slice,
  10. // types
  11. OBJECT = 'Object',
  12. ARRAY = 'Array',
  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. // Array Mutation Handlers & Augmentations ------------------------------------
  21. // The proxy prototype to replace the __proto__ of
  22. // an observed array
  23. var ArrayProxy = Object.create(Array.prototype)
  24. // intercept mutation methods
  25. ;[
  26. 'push',
  27. 'pop',
  28. 'shift',
  29. 'unshift',
  30. 'splice',
  31. 'sort',
  32. 'reverse'
  33. ].forEach(watchMutation)
  34. // Augment the ArrayProxy with convenience methods
  35. def(ArrayProxy, '$set', function (index, data) {
  36. return this.splice(index, 1, data)[0]
  37. }, !hasProto)
  38. def(ArrayProxy, '$remove', function (index) {
  39. if (typeof index !== 'number') {
  40. index = this.indexOf(index)
  41. }
  42. if (index > -1) {
  43. return this.splice(index, 1)[0]
  44. }
  45. }, !hasProto)
  46. /**
  47. * Intercep a mutation event so we can emit the mutation info.
  48. * we also analyze what elements are added/removed and link/unlink
  49. * them with the parent Array.
  50. */
  51. function watchMutation (method) {
  52. def(ArrayProxy, method, function () {
  53. var args = slice.call(arguments),
  54. result = Array.prototype[method].apply(this, args),
  55. inserted, removed
  56. // determine new / removed elements
  57. if (method === 'push' || method === 'unshift') {
  58. inserted = args
  59. } else if (method === 'pop' || method === 'shift') {
  60. removed = [result]
  61. } else if (method === 'splice') {
  62. inserted = args.slice(2)
  63. removed = result
  64. }
  65. // link & unlink
  66. linkArrayElements(this, inserted)
  67. unlinkArrayElements(this, removed)
  68. // emit the mutation event
  69. this.__emitter__.emit('mutate', '', this, {
  70. method : method,
  71. args : args,
  72. result : result,
  73. inserted : inserted,
  74. removed : removed
  75. })
  76. return result
  77. }, !hasProto)
  78. }
  79. /**
  80. * Link new elements to an Array, so when they change
  81. * and emit events, the owner Array can be notified.
  82. */
  83. function linkArrayElements (arr, items) {
  84. if (items) {
  85. var i = items.length, item, owners
  86. while (i--) {
  87. item = items[i]
  88. if (isWatchable(item)) {
  89. // if object is not converted for observing
  90. // convert it...
  91. if (!item.__emitter__) {
  92. convert(item)
  93. watch(item)
  94. }
  95. owners = item.__emitter__.owners
  96. if (owners.indexOf(arr) < 0) {
  97. owners.push(arr)
  98. }
  99. }
  100. }
  101. }
  102. }
  103. /**
  104. * Unlink removed elements from the ex-owner Array.
  105. */
  106. function unlinkArrayElements (arr, items) {
  107. if (items) {
  108. var i = items.length, item
  109. while (i--) {
  110. item = items[i]
  111. if (item && item.__emitter__) {
  112. var owners = item.__emitter__.owners
  113. if (owners) owners.splice(owners.indexOf(arr))
  114. }
  115. }
  116. }
  117. }
  118. // Object add/delete key augmentation -----------------------------------------
  119. var ObjProxy = Object.create(Object.prototype)
  120. def(ObjProxy, '$add', function (key, val) {
  121. if (hasOwn.call(this, key)) return
  122. this[key] = val
  123. convertKey(this, key)
  124. // emit a propagating set event
  125. this.__emitter__.emit('set', key, val, true)
  126. }, !hasProto)
  127. def(ObjProxy, '$delete', function (key) {
  128. if (!(hasOwn.call(this, key))) return
  129. // trigger set events
  130. this[key] = undefined
  131. delete this[key]
  132. this.__emitter__.emit('delete', key)
  133. }, !hasProto)
  134. // Watch Helpers --------------------------------------------------------------
  135. /**
  136. * Check if a value is watchable
  137. */
  138. function isWatchable (obj) {
  139. ViewModel = ViewModel || require('./viewmodel')
  140. var type = typeOf(obj)
  141. return (type === OBJECT || type === ARRAY) && !(obj instanceof ViewModel)
  142. }
  143. /**
  144. * Convert an Object/Array to give it a change emitter.
  145. */
  146. function convert (obj) {
  147. if (obj.__emitter__) return true
  148. var emitter = new Emitter()
  149. def(obj, '__emitter__', emitter)
  150. emitter
  151. .on('set', function (key, val, propagate) {
  152. if (propagate) propagateChange(obj)
  153. })
  154. .on('mutate', function () {
  155. propagateChange(obj)
  156. })
  157. emitter.values = utils.hash()
  158. emitter.owners = []
  159. return false
  160. }
  161. /**
  162. * Propagate an array element's change to its owner arrays
  163. */
  164. function propagateChange (obj) {
  165. var owners = obj.__emitter__.owners,
  166. i = owners.length
  167. while (i--) {
  168. owners[i].__emitter__.emit('set', '', '', true)
  169. }
  170. }
  171. /**
  172. * Watch target based on its type
  173. */
  174. function watch (obj) {
  175. var type = typeOf(obj)
  176. if (type === OBJECT) {
  177. watchObject(obj)
  178. } else if (type === ARRAY) {
  179. watchArray(obj)
  180. }
  181. }
  182. /**
  183. * Augment target objects with modified
  184. * methods
  185. */
  186. function augment (target, src) {
  187. if (hasProto) {
  188. target.__proto__ = src
  189. } else {
  190. for (var key in src) {
  191. def(target, key, src[key])
  192. }
  193. }
  194. }
  195. /**
  196. * Watch an Object, recursive.
  197. */
  198. function watchObject (obj) {
  199. augment(obj, ObjProxy)
  200. for (var key in obj) {
  201. convertKey(obj, key)
  202. }
  203. }
  204. /**
  205. * Watch an Array, overload mutation methods
  206. * and add augmentations by intercepting the prototype chain
  207. */
  208. function watchArray (arr) {
  209. augment(arr, ArrayProxy)
  210. linkArrayElements(arr, arr)
  211. }
  212. /**
  213. * Define accessors for a property on an Object
  214. * so it emits get/set events.
  215. * Then watch the value itself.
  216. */
  217. function convertKey (obj, key) {
  218. var keyPrefix = key.charAt(0)
  219. if (keyPrefix === '$' || keyPrefix === '_') {
  220. return
  221. }
  222. // emit set on bind
  223. // this means when an object is observed it will emit
  224. // a first batch of set events.
  225. var emitter = obj.__emitter__,
  226. values = emitter.values
  227. init(obj[key])
  228. oDef(obj, key, {
  229. enumerable: true,
  230. configurable: true,
  231. get: function () {
  232. var value = values[key]
  233. // only emit get on tip values
  234. if (pub.shouldGet) {
  235. emitter.emit('get', key)
  236. }
  237. return value
  238. },
  239. set: function (newVal) {
  240. var oldVal = values[key]
  241. unobserve(oldVal, key, emitter)
  242. copyPaths(newVal, oldVal)
  243. // an immediate property should notify its parent
  244. // to emit set for itself too
  245. init(newVal, true)
  246. }
  247. })
  248. function init (val, propagate) {
  249. values[key] = val
  250. emitter.emit('set', key, val, propagate)
  251. if (Array.isArray(val)) {
  252. emitter.emit('set', key + '.length', val.length, propagate)
  253. }
  254. observe(val, key, emitter)
  255. }
  256. }
  257. /**
  258. * When a value that is already converted is
  259. * observed again by another observer, we can skip
  260. * the watch conversion and simply emit set event for
  261. * all of its properties.
  262. */
  263. function emitSet (obj) {
  264. var type = typeOf(obj),
  265. emitter = obj && obj.__emitter__
  266. if (type === ARRAY) {
  267. emitter.emit('set', 'length', obj.length)
  268. } else if (type === OBJECT) {
  269. var key, val
  270. for (key in obj) {
  271. val = obj[key]
  272. emitter.emit('set', key, val)
  273. emitSet(val)
  274. }
  275. }
  276. }
  277. /**
  278. * Make sure all the paths in an old object exists
  279. * in a new object.
  280. * So when an object changes, all missing keys will
  281. * emit a set event with undefined value.
  282. */
  283. function copyPaths (newObj, oldObj) {
  284. if (typeOf(oldObj) !== OBJECT || typeOf(newObj) !== OBJECT) {
  285. return
  286. }
  287. var path, type, oldVal, newVal
  288. for (path in oldObj) {
  289. if (!(hasOwn.call(newObj, path))) {
  290. oldVal = oldObj[path]
  291. type = typeOf(oldVal)
  292. if (type === OBJECT) {
  293. newVal = newObj[path] = {}
  294. copyPaths(newVal, oldVal)
  295. } else if (type === ARRAY) {
  296. newObj[path] = []
  297. } else {
  298. newObj[path] = undefined
  299. }
  300. }
  301. }
  302. }
  303. /**
  304. * walk along a path and make sure it can be accessed
  305. * and enumerated in that object
  306. */
  307. function ensurePath (obj, key) {
  308. var path = key.split('.'), sec
  309. for (var i = 0, d = path.length - 1; i < d; i++) {
  310. sec = path[i]
  311. if (!obj[sec]) {
  312. obj[sec] = {}
  313. if (obj.__emitter__) convertKey(obj, sec)
  314. }
  315. obj = obj[sec]
  316. }
  317. if (typeOf(obj) === OBJECT) {
  318. sec = path[i]
  319. if (!(hasOwn.call(obj, sec))) {
  320. obj[sec] = undefined
  321. if (obj.__emitter__) convertKey(obj, sec)
  322. }
  323. }
  324. }
  325. // Main API Methods -----------------------------------------------------------
  326. /**
  327. * Observe an object with a given path,
  328. * and proxy get/set/mutate events to the provided observer.
  329. */
  330. function observe (obj, rawPath, observer) {
  331. if (!isWatchable(obj)) return
  332. var path = rawPath ? rawPath + '.' : '',
  333. alreadyConverted = convert(obj),
  334. emitter = obj.__emitter__
  335. // setup proxy listeners on the parent observer.
  336. // we need to keep reference to them so that they
  337. // can be removed when the object is un-observed.
  338. observer.proxies = observer.proxies || {}
  339. var proxies = observer.proxies[path] = {
  340. get: function (key) {
  341. observer.emit('get', path + key)
  342. },
  343. set: function (key, val, propagate) {
  344. if (key) observer.emit('set', path + key, val)
  345. // also notify observer that the object itself changed
  346. // but only do so when it's a immediate property. this
  347. // avoids duplicate event firing.
  348. if (rawPath && propagate) {
  349. observer.emit('set', rawPath, obj, true)
  350. }
  351. },
  352. mutate: function (key, val, mutation) {
  353. // if the Array is a root value
  354. // the key will be null
  355. var fixedPath = key ? path + key : rawPath
  356. observer.emit('mutate', fixedPath, val, mutation)
  357. // also emit set for Array's length when it mutates
  358. var m = mutation.method
  359. if (m !== 'sort' && m !== 'reverse') {
  360. observer.emit('set', fixedPath + '.length', val.length)
  361. }
  362. }
  363. }
  364. // attach the listeners to the child observer.
  365. // now all the events will propagate upwards.
  366. emitter
  367. .on('get', proxies.get)
  368. .on('set', proxies.set)
  369. .on('mutate', proxies.mutate)
  370. if (alreadyConverted) {
  371. // for objects that have already been converted,
  372. // emit set events for everything inside
  373. emitSet(obj)
  374. } else {
  375. watch(obj)
  376. }
  377. }
  378. /**
  379. * Cancel observation, turn off the listeners.
  380. */
  381. function unobserve (obj, path, observer) {
  382. if (!obj || !obj.__emitter__) return
  383. path = path ? path + '.' : ''
  384. var proxies = observer.proxies[path]
  385. if (!proxies) return
  386. // turn off listeners
  387. obj.__emitter__
  388. .off('get', proxies.get)
  389. .off('set', proxies.set)
  390. .off('mutate', proxies.mutate)
  391. // remove reference
  392. observer.proxies[path] = null
  393. }
  394. // Expose API -----------------------------------------------------------------
  395. var pub = module.exports = {
  396. // whether to emit get events
  397. // only enabled during dependency parsing
  398. shouldGet : false,
  399. observe : observe,
  400. unobserve : unobserve,
  401. ensurePath : ensurePath,
  402. copyPaths : copyPaths,
  403. watch : watch,
  404. convert : convert,
  405. convertKey : convertKey
  406. }