observer.spec.ts 11 KB

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