2
0

observer.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import Vue from 'vue'
  2. import {
  3. Observer,
  4. observe,
  5. set as setProp,
  6. del as delProp
  7. } from 'core/observer/index'
  8. import Dep from 'core/observer/dep'
  9. import { hasOwn } from 'core/util/index'
  10. describe('Observer', () => {
  11. it('create on non-observables', () => {
  12. // skip primitive value
  13. const ob1 = observe(1)
  14. expect(ob1).toBeUndefined()
  15. // avoid vue instance
  16. const ob2 = observe(new Vue())
  17. expect(ob2).toBeUndefined()
  18. // avoid frozen objects
  19. const ob3 = observe(Object.freeze({}))
  20. expect(ob3).toBeUndefined()
  21. })
  22. it('create on object', () => {
  23. // on object
  24. const obj: any = {
  25. a: {},
  26. b: {}
  27. }
  28. const ob1 = observe(obj)!
  29. expect(ob1 instanceof Observer).toBe(true)
  30. expect(ob1.value).toBe(obj)
  31. expect(obj.__ob__).toBe(ob1)
  32. // should've walked children
  33. expect(obj.a.__ob__ instanceof Observer).toBe(true)
  34. expect(obj.b.__ob__ instanceof Observer).toBe(true)
  35. // should return existing ob on already observed objects
  36. const ob2 = observe(obj)!
  37. expect(ob2).toBe(ob1)
  38. })
  39. it('create on null', () => {
  40. // on null
  41. const obj: any =Object.create(null)
  42. obj.a = {}
  43. obj.b = {}
  44. const ob1 = observe(obj)!
  45. expect(ob1 instanceof Observer).toBe(true)
  46. expect(ob1.value).toBe(obj)
  47. expect(obj.__ob__).toBe(ob1)
  48. // should've walked children
  49. expect(obj.a.__ob__ instanceof Observer).toBe(true)
  50. expect(obj.b.__ob__ instanceof Observer).toBe(true)
  51. // should return existing ob on already observed objects
  52. const ob2 = observe(obj)!
  53. expect(ob2).toBe(ob1)
  54. })
  55. it('create on already observed object', () => {
  56. // on object
  57. const obj: any = {}
  58. let val = 0
  59. let getCount = 0
  60. Object.defineProperty(obj, 'a', {
  61. configurable: true,
  62. enumerable: true,
  63. get () {
  64. getCount++
  65. return val
  66. },
  67. set (v) { val = v }
  68. })
  69. const ob1 = observe(obj)!
  70. expect(ob1 instanceof Observer).toBe(true)
  71. expect(ob1.value).toBe(obj)
  72. expect(obj.__ob__).toBe(ob1)
  73. getCount = 0
  74. // Each read of 'a' should result in only one get underlying get call
  75. obj.a
  76. expect(getCount).toBe(1)
  77. obj.a
  78. expect(getCount).toBe(2)
  79. // should return existing ob on already observed objects
  80. const ob2 = observe(obj)!
  81. expect(ob2).toBe(ob1)
  82. // should call underlying setter
  83. obj.a = 10
  84. expect(val).toBe(10)
  85. })
  86. it('create on property with only getter', () => {
  87. // on object
  88. const obj: any = {}
  89. Object.defineProperty(obj, 'a', {
  90. configurable: true,
  91. enumerable: true,
  92. get () { return 123 }
  93. })
  94. const ob1 = observe(obj)!
  95. expect(ob1 instanceof Observer).toBe(true)
  96. expect(ob1.value).toBe(obj)
  97. expect(obj.__ob__).toBe(ob1)
  98. // should be able to read
  99. expect(obj.a).toBe(123)
  100. // should return existing ob on already observed objects
  101. const ob2 = observe(obj)!
  102. expect(ob2).toBe(ob1)
  103. // since there is no setter, you shouldn't be able to write to it
  104. // PhantomJS throws when a property with no setter is set
  105. // but other real browsers don't
  106. try {
  107. obj.a = 101
  108. } catch (e) {}
  109. expect(obj.a).toBe(123)
  110. })
  111. it('create on property with only setter', () => {
  112. // on object
  113. const obj: any ={}
  114. let val = 10
  115. Object.defineProperty(obj, 'a', { // eslint-disable-line accessor-pairs
  116. configurable: true,
  117. enumerable: true,
  118. set (v) { val = v }
  119. })
  120. const ob1 = observe(obj)!
  121. expect(ob1 instanceof Observer).toBe(true)
  122. expect(ob1.value).toBe(obj)
  123. expect(obj.__ob__).toBe(ob1)
  124. // reads should return undefined
  125. expect(obj.a).toBe(undefined)
  126. // should return existing ob on already observed objects
  127. const ob2 = observe(obj)!
  128. expect(ob2).toBe(ob1)
  129. // writes should call the set function
  130. obj.a = 100
  131. expect(val).toBe(100)
  132. })
  133. it('create on property which is marked not configurable', () => {
  134. // on object
  135. const obj: any ={}
  136. Object.defineProperty(obj, 'a', {
  137. configurable: false,
  138. enumerable: true,
  139. value: 10
  140. })
  141. const ob1 = observe(obj)!
  142. expect(ob1 instanceof Observer).toBe(true)
  143. expect(ob1.value).toBe(obj)
  144. expect(obj.__ob__).toBe(ob1)
  145. })
  146. it('create on array', () => {
  147. // on object
  148. const arr: any = [{}, {}]
  149. const ob1 = observe(arr)!
  150. expect(ob1 instanceof Observer).toBe(true)
  151. expect(ob1.value).toBe(arr)
  152. expect(arr.__ob__).toBe(ob1)
  153. // should've walked children
  154. expect(arr[0].__ob__ instanceof Observer).toBe(true)
  155. expect(arr[1].__ob__ instanceof Observer).toBe(true)
  156. })
  157. it('observing object prop change', () => {
  158. const obj: any ={ a: { b: 2 }, c: NaN }
  159. observe(obj)!
  160. // mock a watcher!
  161. const watcher: any = {
  162. deps: [],
  163. addDep (dep) {
  164. this.deps.push(dep)
  165. dep.addSub(this)
  166. },
  167. update: vi.fn()
  168. }
  169. // collect dep
  170. Dep.target = watcher
  171. obj.a.b
  172. Dep.target = null
  173. expect(watcher.deps.length).toBe(3) // obj.a + a + a.b
  174. obj.a.b = 3
  175. expect(watcher.update.mock.calls.length).toBe(1)
  176. // swap object
  177. obj.a = { b: 4 }
  178. expect(watcher.update.mock.calls.length).toBe(2)
  179. watcher.deps = []
  180. Dep.target = watcher
  181. obj.a.b
  182. obj.c
  183. Dep.target = null
  184. expect(watcher.deps.length).toBe(4)
  185. // set on the swapped object
  186. obj.a.b = 5
  187. expect(watcher.update.mock.calls.length).toBe(3)
  188. // should not trigger on NaN -> NaN set
  189. obj.c = NaN
  190. expect(watcher.update.mock.calls.length).toBe(3)
  191. })
  192. it('observing object prop change on defined property', () => {
  193. const obj: any ={ val: 2 }
  194. Object.defineProperty(obj, 'a', {
  195. configurable: true,
  196. enumerable: true,
  197. get () { return this.val },
  198. set (v) {
  199. this.val = v
  200. // eslint-disable-next-line no-setter-return
  201. return this.val
  202. }
  203. })
  204. observe(obj)!
  205. expect(obj.a).toBe(2) // Make sure 'this' is preserved
  206. obj.a = 3
  207. expect(obj.val).toBe(3) // make sure 'setter' was called
  208. obj.val = 5
  209. expect(obj.a).toBe(5) // make sure 'getter' was called
  210. })
  211. it('observing set/delete', () => {
  212. const obj1: any = { a: 1 }
  213. const ob1 = observe(obj1) as any
  214. const dep1 = ob1.dep
  215. vi.spyOn(dep1, 'notify')
  216. setProp(obj1, 'b', 2)
  217. expect(obj1.b).toBe(2)
  218. expect(dep1.notify.mock.calls.length).toBe(1)
  219. delProp(obj1, 'a')
  220. expect(hasOwn(obj1, 'a')).toBe(false)
  221. expect(dep1.notify.mock.calls.length).toBe(2)
  222. // set existing key, should be a plain set and not
  223. // trigger own ob's notify
  224. setProp(obj1, 'b', 3)
  225. expect(obj1.b).toBe(3)
  226. expect(dep1.notify.mock.calls.length).toBe(2)
  227. // set non-existing key
  228. setProp(obj1, 'c', 1)
  229. expect(obj1.c).toBe(1)
  230. expect(dep1.notify.mock.calls.length).toBe(3)
  231. // should ignore deleting non-existing key
  232. delProp(obj1, 'a')
  233. expect(dep1.notify.mock.calls.length).toBe(3)
  234. // should work on non-observed objects
  235. const obj2 = { a: 1 }
  236. delProp(obj2, 'a')
  237. expect(hasOwn(obj2, 'a')).toBe(false)
  238. // should work on Object.create(null)
  239. const obj3: any = Object.create(null)
  240. obj3.a = 1
  241. const ob3 = observe(obj3) as any
  242. const dep3 = ob3.dep
  243. vi.spyOn(dep3, 'notify')
  244. setProp(obj3, 'b', 2)
  245. expect(obj3.b).toBe(2)
  246. expect(dep3.notify.mock.calls.length).toBe(1)
  247. delProp(obj3, 'a')
  248. expect(hasOwn(obj3, 'a')).toBe(false)
  249. expect(dep3.notify.mock.calls.length).toBe(2)
  250. // set and delete non-numeric key on array
  251. const arr2: any = ['a']
  252. const ob2 = observe(arr2) as any
  253. const dep2 = ob2.dep
  254. vi.spyOn(dep2, 'notify')
  255. setProp(arr2, 'b', 2)
  256. expect(arr2.b).toBe(2)
  257. expect(dep2.notify.mock.calls.length).toBe(1)
  258. delProp(arr2, 'b')
  259. expect(hasOwn(arr2, 'b')).toBe(false)
  260. expect(dep2.notify.mock.calls.length).toBe(2)
  261. })
  262. it('warning set/delete on a Vue instance', done => {
  263. const vm = new Vue({
  264. template: '<div>{{a}}</div>',
  265. data: { a: 1 }
  266. }).$mount()
  267. expect(vm.$el.outerHTML).toBe('<div>1</div>')
  268. Vue.set(vm, 'a', 2)
  269. waitForUpdate(() => {
  270. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  271. expect('Avoid adding reactive properties to a Vue instance').not.toHaveBeenWarned()
  272. Vue.delete(vm, 'a')
  273. }).then(() => {
  274. expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
  275. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  276. Vue.set(vm, 'b', 123)
  277. expect('Avoid adding reactive properties to a Vue instance').toHaveBeenWarned()
  278. }).then(done)
  279. })
  280. it('warning set/delete on Vue instance root $data', done => {
  281. const data = { a: 1 }
  282. const vm = new Vue({
  283. template: '<div>{{a}}</div>',
  284. data
  285. }).$mount()
  286. expect(vm.$el.outerHTML).toBe('<div>1</div>')
  287. expect(Vue.set(data, 'a', 2)).toBe(2)
  288. waitForUpdate(() => {
  289. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  290. expect('Avoid adding reactive properties to a Vue instance').not.toHaveBeenWarned()
  291. Vue.delete(data, 'a')
  292. }).then(() => {
  293. expect('Avoid deleting properties on a Vue instance').toHaveBeenWarned()
  294. expect(vm.$el.outerHTML).toBe('<div>2</div>')
  295. expect(Vue.set(data, 'b', 123)).toBe(123)
  296. expect('Avoid adding reactive properties to a Vue instance').toHaveBeenWarned()
  297. }).then(done)
  298. })
  299. it('observing array mutation', () => {
  300. const arr: any[] = []
  301. const ob = observe(arr) as any
  302. const dep = ob.dep
  303. vi.spyOn(dep, 'notify')
  304. const objs = [{}, {}, {}]
  305. arr.push(objs[0])
  306. arr.pop()
  307. arr.unshift(objs[1])
  308. arr.shift()
  309. arr.splice(0, 0, objs[2])
  310. arr.sort()
  311. arr.reverse()
  312. expect(dep.notify.mock.calls.length).toBe(7)
  313. // inserted elements should be observed
  314. objs.forEach((obj: any) => {
  315. expect(obj.__ob__ instanceof Observer).toBe(true)
  316. })
  317. })
  318. it('warn set/delete on non valid values', () => {
  319. try {
  320. // @ts-expect-error
  321. setProp(null, 'foo', 1)
  322. } catch (e) {}
  323. expect(`Cannot set reactive property on undefined, null, or primitive value`).toHaveBeenWarned()
  324. try {
  325. // @ts-expect-error
  326. delProp(null, 'foo')
  327. } catch (e) {}
  328. expect(`Cannot delete reactive property on undefined, null, or primitive value`).toHaveBeenWarned()
  329. })
  330. it('should lazy invoke existing getters', () => {
  331. const obj: any ={}
  332. let called = false
  333. Object.defineProperty(obj, 'getterProp', {
  334. enumerable: true,
  335. get: () => {
  336. called = true
  337. return 'some value'
  338. }
  339. })
  340. observe(obj)!
  341. expect(called).toBe(false)
  342. })
  343. })