renderEffect.spec.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import {
  2. EffectScope,
  3. type GenericComponentInstance,
  4. currentInstance,
  5. getCurrentScope,
  6. nextTick,
  7. onBeforeUpdate,
  8. onUpdated,
  9. ref,
  10. watchEffect,
  11. watchPostEffect,
  12. watchSyncEffect,
  13. } from '@vue/runtime-dom'
  14. import { renderEffect, template } from '../src'
  15. import { RenderEffect } from '../src/renderEffect'
  16. import { onEffectCleanup } from '@vue/reactivity'
  17. import { makeRender } from './_utils'
  18. const define = makeRender<any>()
  19. const createDemo = (setupFn: () => any, renderFn: (ctx: any) => any) =>
  20. define({
  21. setup: () => {
  22. const returned = setupFn()
  23. Object.defineProperty(returned, '__isScriptSetup', {
  24. enumerable: false,
  25. value: true,
  26. })
  27. return returned
  28. },
  29. render: (ctx: any) => {
  30. const t0 = template('<div></div>')
  31. renderFn(ctx)
  32. return t0()
  33. },
  34. })
  35. describe('renderEffect', () => {
  36. test('initializes noLifecycle effect with raw effect function', () => {
  37. let calls = 0
  38. const fn = () => {
  39. calls++
  40. }
  41. const effect = new RenderEffect(fn, true)
  42. expect(effect.fn).toBe(fn)
  43. expect(effect.updateJob).toBe(undefined)
  44. effect.run()
  45. expect(calls).toBe(1)
  46. })
  47. test('creates update lifecycle job lazily', async () => {
  48. const effect = new RenderEffect(() => {})
  49. expect(effect.updateJob).toBe(undefined)
  50. const effects: RenderEffect[] = []
  51. const calls: string[] = []
  52. const { instance } = createDemo(
  53. () => {
  54. const source = ref(0)
  55. const update = () => source.value++
  56. onUpdated(() => calls.push(`updated ${source.value}`))
  57. return { source, update }
  58. },
  59. ctx => {
  60. const effect = new RenderEffect(() => {
  61. calls.push(`render ${ctx.source}`)
  62. })
  63. effects.push(effect)
  64. effect.run()
  65. },
  66. ).render()
  67. expect(effects[0].updateJob).toBe(undefined)
  68. expect(calls).toEqual(['render 0'])
  69. const { update } = instance?.setupState as any
  70. update()
  71. await nextTick()
  72. expect(effects[0].updateJob).toEqual(expect.any(Function))
  73. expect(calls).toEqual(['render 0', 'render 1', 'updated 1'])
  74. })
  75. test('creates update lifecycle job after hooks are registered late', async () => {
  76. const effects: RenderEffect[] = []
  77. const calls: string[] = []
  78. const { instance } = createDemo(
  79. () => {
  80. const source = ref(0)
  81. const update = () => source.value++
  82. const effect = new RenderEffect(() => {
  83. calls.push(`render ${source.value}`)
  84. })
  85. effects.push(effect)
  86. effect.run()
  87. onUpdated(() => calls.push(`updated ${source.value}`))
  88. return { update }
  89. },
  90. () => {},
  91. ).render()
  92. expect(effects[0].updateJob).toBe(undefined)
  93. expect(calls).toEqual(['render 0'])
  94. const { update } = instance?.setupState as any
  95. update()
  96. await nextTick()
  97. expect(effects[0].updateJob).toEqual(expect.any(Function))
  98. expect(calls).toEqual(['render 0', 'render 1', 'updated 1'])
  99. })
  100. test('basic', async () => {
  101. let dummy: any
  102. const source = ref(0)
  103. renderEffect(() => {
  104. dummy = source.value
  105. })
  106. expect(dummy).toBe(0)
  107. await nextTick()
  108. expect(dummy).toBe(0)
  109. source.value++
  110. expect(dummy).toBe(0)
  111. await nextTick()
  112. expect(dummy).toBe(1)
  113. source.value++
  114. expect(dummy).toBe(1)
  115. await nextTick()
  116. expect(dummy).toBe(2)
  117. source.value++
  118. expect(dummy).toBe(2)
  119. await nextTick()
  120. expect(dummy).toBe(3)
  121. })
  122. test('should run with the scheduling order', async () => {
  123. const calls: string[] = []
  124. const { instance } = createDemo(
  125. () => {
  126. // setup
  127. const source = ref(0)
  128. const renderSource = ref(0)
  129. const change = () => source.value++
  130. const changeRender = () => renderSource.value++
  131. // Life Cycle Hooks
  132. onUpdated(() => {
  133. calls.push(`updated ${source.value}`)
  134. })
  135. onBeforeUpdate(() => {
  136. calls.push(`beforeUpdate ${source.value}`)
  137. })
  138. // Watch API
  139. watchPostEffect(() => {
  140. const current = source.value
  141. calls.push(`post ${current}`)
  142. onEffectCleanup(() => calls.push(`post cleanup ${current}`))
  143. })
  144. watchEffect(() => {
  145. const current = source.value
  146. calls.push(`pre ${current}`)
  147. onEffectCleanup(() => calls.push(`pre cleanup ${current}`))
  148. })
  149. watchSyncEffect(() => {
  150. const current = source.value
  151. calls.push(`sync ${current}`)
  152. onEffectCleanup(() => calls.push(`sync cleanup ${current}`))
  153. })
  154. return { source, change, renderSource, changeRender }
  155. },
  156. // render
  157. _ctx => {
  158. // Render Watch API
  159. renderEffect(() => {
  160. const current = _ctx.renderSource
  161. calls.push(`renderEffect ${current}`)
  162. onEffectCleanup(() => calls.push(`renderEffect cleanup ${current}`))
  163. })
  164. },
  165. ).render()
  166. const { change, changeRender } = instance?.setupState as any
  167. await nextTick()
  168. expect(calls).toEqual(['pre 0', 'sync 0', 'renderEffect 0', 'post 0'])
  169. calls.length = 0
  170. // Update
  171. changeRender()
  172. change()
  173. expect(calls).toEqual(['sync cleanup 0', 'sync 1'])
  174. calls.length = 0
  175. await nextTick()
  176. expect(calls).toEqual([
  177. 'pre cleanup 0',
  178. 'pre 1',
  179. 'renderEffect cleanup 0',
  180. 'beforeUpdate 1',
  181. 'renderEffect 1',
  182. 'post cleanup 0',
  183. 'post 1',
  184. 'updated 1',
  185. ])
  186. calls.length = 0
  187. // Update
  188. changeRender()
  189. change()
  190. expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
  191. calls.length = 0
  192. await nextTick()
  193. expect(calls).toEqual([
  194. 'pre cleanup 1',
  195. 'pre 2',
  196. 'renderEffect cleanup 1',
  197. 'beforeUpdate 2',
  198. 'renderEffect 2',
  199. 'post cleanup 1',
  200. 'post 2',
  201. 'updated 2',
  202. ])
  203. })
  204. test('errors should include the execution location with beforeUpdate hook', async () => {
  205. const { instance } = createDemo(
  206. // setup
  207. () => {
  208. const source = ref()
  209. const update = () => source.value++
  210. onBeforeUpdate(() => {
  211. throw 'error in beforeUpdate'
  212. })
  213. return { source, update }
  214. },
  215. // render
  216. ctx => {
  217. renderEffect(() => {
  218. ctx.source
  219. })
  220. },
  221. ).render()
  222. const { update } = instance?.setupState as any
  223. await expect(async () => {
  224. update()
  225. await nextTick()
  226. }).rejects.toThrow('error in beforeUpdate')
  227. expect(
  228. '[Vue warn]: Unhandled error during execution of beforeUpdate hook',
  229. ).toHaveBeenWarned()
  230. expect(
  231. '[Vue warn]: Unhandled error during execution of component update',
  232. ).toHaveBeenWarned()
  233. })
  234. test('should restore update state when render throws during update', async () => {
  235. const calls: string[] = []
  236. const { instance } = createDemo(
  237. // setup
  238. () => {
  239. const source = ref(0)
  240. const update = () => source.value++
  241. onBeforeUpdate(() => calls.push(`beforeUpdate ${source.value}`))
  242. onUpdated(() => calls.push(`updated ${source.value}`))
  243. return { source, update }
  244. },
  245. // render
  246. ctx => {
  247. renderEffect(() => {
  248. calls.push(`render ${ctx.source}`)
  249. if (ctx.source === 1) {
  250. throw new Error('error in render')
  251. }
  252. })
  253. },
  254. ).render()
  255. const { update } = instance?.setupState as any
  256. expect(calls).toEqual(['render 0'])
  257. calls.length = 0
  258. update()
  259. await expect(nextTick()).rejects.toThrow('error in render')
  260. expect(
  261. '[Vue warn]: Unhandled error during execution of component update',
  262. ).toHaveBeenWarned()
  263. expect(currentInstance).toBe(null)
  264. expect((instance as any).isUpdating).toBe(false)
  265. calls.length = 0
  266. update()
  267. await nextTick()
  268. expect(calls).toEqual(['beforeUpdate 2', 'render 2', 'updated 2'])
  269. expect((instance as any).isUpdating).toBe(false)
  270. })
  271. test('errors should include the execution location with updated hook', async () => {
  272. const { instance } = createDemo(
  273. // setup
  274. () => {
  275. const source = ref(0)
  276. const update = () => source.value++
  277. onUpdated(() => {
  278. throw 'error in updated'
  279. })
  280. return { source, update }
  281. },
  282. // render
  283. ctx => {
  284. renderEffect(() => {
  285. ctx.source
  286. })
  287. },
  288. ).render()
  289. const { update } = instance?.setupState as any
  290. await expect(async () => {
  291. update()
  292. await nextTick()
  293. }).rejects.toThrow('error in updated')
  294. expect(
  295. '[Vue warn]: Unhandled error during execution of updated',
  296. ).toHaveBeenWarned()
  297. })
  298. test('should be called with the current instance and current scope', async () => {
  299. const source = ref(0)
  300. const scope = new EffectScope()
  301. let instanceSnap: GenericComponentInstance | null = null
  302. let scopeSnap: EffectScope | undefined = undefined
  303. const { instance } = define(() => {
  304. scope.run(() => {
  305. renderEffect(() => {
  306. source.value
  307. instanceSnap = currentInstance
  308. scopeSnap = getCurrentScope()
  309. })
  310. })
  311. return []
  312. }).render()
  313. expect(instanceSnap).toBe(instance)
  314. expect(scopeSnap).toBe(scope)
  315. source.value++
  316. await nextTick()
  317. expect(instanceSnap).toBe(instance)
  318. expect(scopeSnap).toBe(scope)
  319. })
  320. })