scope.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. var _ = require('../util')
  2. var Observer = require('../observe/observer')
  3. var scopeEvents = ['set', 'mutate', 'add', 'delete']
  4. /**
  5. * Setup instance scope.
  6. * The scope is reponsible for prototypal inheritance of
  7. * parent instance propertiesm abd all binding paths and
  8. * expressions of the current instance are evaluated against
  9. * its scope.
  10. *
  11. * This should only be called once during _init().
  12. */
  13. exports._initScope = function () {
  14. var parent = this.$parent
  15. var inherit = parent && !this.$options.isolated
  16. var data = this._data
  17. var scope = this.$scope = inherit
  18. ? Object.create(parent.$scope)
  19. : {}
  20. // copy initial data into scope
  21. var keys = Object.keys(data)
  22. var i = keys.length
  23. while (i--) {
  24. // use defineProperty so we can shadow parent accessors
  25. key = keys[i]
  26. _.define(scope, key, data[key], true)
  27. }
  28. // create scope observer
  29. this.$observer = Observer.create(scope, {
  30. callbackContext: this,
  31. doNotAlterProto: true
  32. })
  33. // setup sync between data and the scope
  34. this._syncData()
  35. if (!inherit) {
  36. return
  37. }
  38. // relay change events that sent down from
  39. // the scope prototype chain.
  40. var ob = this.$observer
  41. var pob = parent.$observer
  42. var listeners = this._scopeListeners = {}
  43. scopeEvents.forEach(function (event) {
  44. var cb = listeners[event] = function (key, a, b) {
  45. // since these events come from upstream,
  46. // we only emit them if we don't have the same keys
  47. // shadowing them in current scope.
  48. if (!scope.hasOwnProperty(key)) {
  49. ob.emit(event, key, a, b, true)
  50. }
  51. }
  52. pob.on(event, cb)
  53. })
  54. }
  55. /**
  56. * Teardown scope, unsync data, and remove all listeners
  57. * including ones attached to parent's observer.
  58. * Only called once during $destroy().
  59. */
  60. exports._teardownScope = function () {
  61. this.$observer.off()
  62. this._unsyncData()
  63. this._data = null
  64. this.$scope = null
  65. if (this.$parent) {
  66. var pob = this.$parent.$observer
  67. var listeners = this._scopeListeners
  68. scopeEvents.forEach(function (event) {
  69. pob.off(event, listeners[event])
  70. })
  71. this._scopeListeners = null
  72. }
  73. }
  74. /**
  75. * Called when swapping the $data object.
  76. *
  77. * Old properties that are not present in new data are
  78. * deleted from the scope, and new data properties not
  79. * already on the scope are added. Teardown old data sync
  80. * listeners and setup new ones.
  81. *
  82. * @param {Object} data
  83. */
  84. exports._setData = function (data) {
  85. this._data = data
  86. var scope = this.$scope
  87. var key
  88. // teardown old sync listeners
  89. this._unsyncData()
  90. // delete keys not present in the new data
  91. for (key in scope) {
  92. if (
  93. key.charCodeAt(0) !== 0x24 && // $
  94. scope.hasOwnProperty(key) &&
  95. !(key in data)
  96. ) {
  97. scope.$delete(key)
  98. }
  99. }
  100. // copy properties into scope
  101. for (key in data) {
  102. if (scope.hasOwnProperty(key)) {
  103. // existing property, trigger set
  104. scope[key] = data[key]
  105. } else {
  106. // new property
  107. scope.$add(key, data[key])
  108. }
  109. }
  110. // setup sync between scope and new data
  111. if (!this.$options._noSync) {
  112. this._syncData()
  113. }
  114. }
  115. /**
  116. * Proxy the scope properties on the instance itself,
  117. * so that vm.a === vm.$scope.a.
  118. *
  119. * Note this only proxies *local* scope properties. We want
  120. * to prevent child instances accidentally modifying
  121. * properties with the same name up in the scope chain
  122. * because scope perperties are all getter/setters.
  123. *
  124. * To access parent properties through prototypal fall
  125. * through, access it on the instance's $scope.
  126. *
  127. * This should only be called once during _init().
  128. */
  129. exports._initProxy = function () {
  130. var scope = this.$scope
  131. // scope --> vm
  132. // proxy scope data on vm
  133. var keys = Object.keys(scope)
  134. var i = keys.length
  135. while (i--) {
  136. _.proxy(this, scope, keys[i])
  137. }
  138. // keep proxying up-to-date with added/deleted keys.
  139. this.$observer
  140. .on('add:self', function (key) {
  141. _.proxy(this, scope, key)
  142. })
  143. .on('delete:self', function (key) {
  144. delete this[key]
  145. })
  146. // vm --> scope
  147. // $parent & $root are read-only on $scope
  148. scope.$parent = this.$parent
  149. scope.$root = this.$root
  150. // proxy $data
  151. _.proxy(scope, this, '$data')
  152. }
  153. /**
  154. * Setup computed properties.
  155. * All computed properties are proxied onto the scope.
  156. * Because they are accessors their `this` context will
  157. * be the instance instead of the scope.
  158. */
  159. function noop () {}
  160. exports._initComputed = function () {
  161. var computed = this.$options.computed
  162. var scope = this.$scope
  163. if (computed) {
  164. for (var key in computed) {
  165. var def = computed[key]
  166. if (typeof def === 'function') {
  167. def = {
  168. get: def,
  169. set: noop
  170. }
  171. }
  172. def.enumerable = true
  173. def.configurable = true
  174. Object.defineProperty(this, key, def)
  175. _.proxy(scope, this, key)
  176. }
  177. }
  178. }
  179. /**
  180. * Setup instance methods.
  181. * Methods are also copied into scope, but they must
  182. * be bound to the instance.
  183. */
  184. exports._initMethods = function () {
  185. var methods = this.$options.methods
  186. var scope = this.$scope
  187. if (methods) {
  188. for (var key in methods) {
  189. var method = methods[key]
  190. this[key] = method
  191. scope[key] = _.bind(method, this)
  192. }
  193. }
  194. }
  195. /**
  196. * Setup two-way sync between the instance scope and
  197. * the original data. Requires teardown.
  198. */
  199. exports._syncData = function () {
  200. var data = this._data
  201. var scope = this.$scope
  202. var locked = false
  203. var listeners = this._syncListeners = {
  204. data: {
  205. set: guard(function (key, val) {
  206. data[key] = val
  207. }),
  208. add: guard(function (key, val) {
  209. data.$add(key, val)
  210. }),
  211. delete: guard(function (key) {
  212. data.$delete(key)
  213. })
  214. },
  215. scope: {
  216. set: guard(function (key, val) {
  217. scope[key] = val
  218. }),
  219. add: guard(function (key, val) {
  220. scope.$add(key, val)
  221. }),
  222. delete: guard(function (key) {
  223. scope.$delete(key)
  224. })
  225. }
  226. }
  227. // sync scope and original data.
  228. this.$observer
  229. .on('set:self', listeners.data.set)
  230. .on('add:self', listeners.data.add)
  231. .on('delete:self', listeners.data.delete)
  232. this._dataObserver = Observer.create(data)
  233. this._dataObserver
  234. .on('set:self', listeners.scope.set)
  235. .on('add:self', listeners.scope.add)
  236. .on('delete:self', listeners.scope.delete)
  237. /**
  238. * The guard function prevents infinite loop
  239. * when syncing between two observers. Also
  240. * filters out properties prefixed with $ or _.
  241. *
  242. * @param {Function} fn
  243. * @return {Function}
  244. */
  245. function guard (fn) {
  246. return function (key, val) {
  247. if (locked) {
  248. return
  249. }
  250. var c = key.charCodeAt(0)
  251. if (c === 0x24 || c === 0x5F) { // $ and _
  252. return
  253. }
  254. locked = true
  255. fn(key, val)
  256. locked = false
  257. }
  258. }
  259. }
  260. /**
  261. * Teardown the sync between scope and previous data object.
  262. */
  263. exports._unsyncData = function () {
  264. var listeners = this._syncListeners
  265. if (!listeners) {
  266. return
  267. }
  268. this.$observer
  269. .off('set:self', listeners.data.set)
  270. .off('add:self', listeners.data.add)
  271. .off('delete:self', listeners.data.delete)
  272. this._dataObserver
  273. .off('set:self', listeners.scope.set)
  274. .off('add:self', listeners.scope.add)
  275. .off('delete:self', listeners.scope.delete)
  276. this._syncListeners = null
  277. }