scope.js 6.9 KB

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