scheduler.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. import {
  2. type SchedulerJob,
  3. SchedulerJobFlags,
  4. flushPostFlushCbs,
  5. flushPreFlushCbs,
  6. invalidateJob,
  7. nextTick,
  8. queueJob,
  9. queuePostFlushCb,
  10. } from '../src/scheduler'
  11. describe('scheduler', () => {
  12. it('nextTick', async () => {
  13. const calls: string[] = []
  14. const dummyThen = Promise.resolve().then()
  15. const job1 = () => {
  16. calls.push('job1')
  17. }
  18. const job2 = () => {
  19. calls.push('job2')
  20. }
  21. nextTick(job1)
  22. job2()
  23. expect(calls.length).toBe(1)
  24. await dummyThen
  25. // job1 will be pushed in nextTick
  26. expect(calls.length).toBe(2)
  27. expect(calls).toMatchObject(['job2', 'job1'])
  28. })
  29. describe('queueJob', () => {
  30. it('basic usage', async () => {
  31. const calls: string[] = []
  32. const job1 = () => {
  33. calls.push('job1')
  34. }
  35. const job2 = () => {
  36. calls.push('job2')
  37. }
  38. queueJob(job1)
  39. queueJob(job2)
  40. expect(calls).toEqual([])
  41. await nextTick()
  42. expect(calls).toEqual(['job1', 'job2'])
  43. })
  44. it("should insert jobs in ascending order of job's id when flushing", async () => {
  45. const calls: string[] = []
  46. const job1 = () => {
  47. calls.push('job1')
  48. queueJob(job2)
  49. queueJob(job3)
  50. }
  51. const job2 = () => {
  52. calls.push('job2')
  53. queueJob(job4)
  54. queueJob(job5)
  55. }
  56. job2.id = 10
  57. const job3 = () => {
  58. calls.push('job3')
  59. }
  60. job3.id = 1
  61. const job4 = () => {
  62. calls.push('job4')
  63. }
  64. const job5 = () => {
  65. calls.push('job5')
  66. }
  67. queueJob(job1)
  68. expect(calls).toEqual([])
  69. await nextTick()
  70. expect(calls).toEqual(['job1', 'job3', 'job2', 'job4', 'job5'])
  71. })
  72. it('should dedupe queued jobs', async () => {
  73. const calls: string[] = []
  74. const job1 = () => {
  75. calls.push('job1')
  76. }
  77. const job2 = () => {
  78. calls.push('job2')
  79. }
  80. queueJob(job1)
  81. queueJob(job2)
  82. queueJob(job1)
  83. queueJob(job2)
  84. expect(calls).toEqual([])
  85. await nextTick()
  86. expect(calls).toEqual(['job1', 'job2'])
  87. })
  88. it('queueJob while flushing', async () => {
  89. const calls: string[] = []
  90. const job1 = () => {
  91. calls.push('job1')
  92. // job2 will be executed after job1 at the same tick
  93. queueJob(job2)
  94. }
  95. const job2 = () => {
  96. calls.push('job2')
  97. }
  98. queueJob(job1)
  99. await nextTick()
  100. expect(calls).toEqual(['job1', 'job2'])
  101. })
  102. })
  103. describe('pre flush jobs', () => {
  104. it('queueJob inside preFlushCb', async () => {
  105. const calls: string[] = []
  106. const job1 = () => {
  107. calls.push('job1')
  108. }
  109. const cb1: SchedulerJob = () => {
  110. // queueJob in postFlushCb
  111. calls.push('cb1')
  112. queueJob(job1)
  113. }
  114. cb1.flags! |= SchedulerJobFlags.PRE
  115. queueJob(cb1)
  116. await nextTick()
  117. expect(calls).toEqual(['cb1', 'job1'])
  118. })
  119. it('queueJob & preFlushCb inside preFlushCb', async () => {
  120. const calls: string[] = []
  121. const job1 = () => {
  122. calls.push('job1')
  123. }
  124. job1.id = 1
  125. const cb1: SchedulerJob = () => {
  126. calls.push('cb1')
  127. queueJob(job1)
  128. // cb2 should execute before the job
  129. queueJob(cb2)
  130. queueJob(cb3)
  131. }
  132. cb1.flags! |= SchedulerJobFlags.PRE
  133. const cb2: SchedulerJob = () => {
  134. calls.push('cb2')
  135. }
  136. cb2.flags! |= SchedulerJobFlags.PRE
  137. cb2.id = 1
  138. const cb3: SchedulerJob = () => {
  139. calls.push('cb3')
  140. }
  141. cb3.flags! |= SchedulerJobFlags.PRE
  142. cb3.id = 1
  143. queueJob(cb1)
  144. await nextTick()
  145. expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1'])
  146. })
  147. it('should insert jobs after pre jobs with the same id', async () => {
  148. const calls: string[] = []
  149. const job1: SchedulerJob = () => {
  150. calls.push('job1')
  151. }
  152. job1.id = 1
  153. job1.flags! |= SchedulerJobFlags.PRE
  154. const job2: SchedulerJob = () => {
  155. calls.push('job2')
  156. queueJob(job5)
  157. queueJob(job6)
  158. }
  159. job2.id = 2
  160. job2.flags! |= SchedulerJobFlags.PRE
  161. const job3: SchedulerJob = () => {
  162. calls.push('job3')
  163. }
  164. job3.id = 2
  165. job3.flags! |= SchedulerJobFlags.PRE
  166. const job4: SchedulerJob = () => {
  167. calls.push('job4')
  168. }
  169. job4.id = 3
  170. job4.flags! |= SchedulerJobFlags.PRE
  171. const job5: SchedulerJob = () => {
  172. calls.push('job5')
  173. }
  174. job5.id = 2
  175. const job6: SchedulerJob = () => {
  176. calls.push('job6')
  177. }
  178. job6.id = 2
  179. job6.flags! |= SchedulerJobFlags.PRE
  180. // We need several jobs to test this properly, otherwise
  181. // findInsertionIndex can yield the correct index by chance
  182. queueJob(job4)
  183. queueJob(job2)
  184. queueJob(job3)
  185. queueJob(job1)
  186. await nextTick()
  187. expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4'])
  188. })
  189. it('preFlushCb inside queueJob', async () => {
  190. const calls: string[] = []
  191. const job1 = () => {
  192. // the only case where a pre-flush cb can be queued inside a job is
  193. // when updating the props of a child component. This is handled
  194. // directly inside `updateComponentPreRender` to avoid non atomic
  195. // cb triggers (#1763)
  196. queueJob(cb1)
  197. queueJob(cb2)
  198. flushPreFlushCbs()
  199. calls.push('job1')
  200. }
  201. const cb1: SchedulerJob = () => {
  202. calls.push('cb1')
  203. // a cb triggers its parent job, which should be skipped
  204. queueJob(job1)
  205. }
  206. cb1.flags! |= SchedulerJobFlags.PRE
  207. const cb2: SchedulerJob = () => {
  208. calls.push('cb2')
  209. }
  210. cb2.flags! |= SchedulerJobFlags.PRE
  211. queueJob(job1)
  212. await nextTick()
  213. expect(calls).toEqual(['cb1', 'cb2', 'job1'])
  214. })
  215. // #3806
  216. it('queue preFlushCb inside postFlushCb', async () => {
  217. const spy = vi.fn()
  218. const cb: SchedulerJob = () => spy()
  219. cb.flags! |= SchedulerJobFlags.PRE
  220. queuePostFlushCb(() => {
  221. queueJob(cb)
  222. })
  223. await nextTick()
  224. expect(spy).toHaveBeenCalled()
  225. })
  226. })
  227. describe('queuePostFlushCb', () => {
  228. it('basic usage', async () => {
  229. const calls: string[] = []
  230. const cb1 = () => {
  231. calls.push('cb1')
  232. }
  233. const cb2 = () => {
  234. calls.push('cb2')
  235. }
  236. const cb3 = () => {
  237. calls.push('cb3')
  238. }
  239. queuePostFlushCb([cb1, cb2])
  240. queuePostFlushCb(cb3)
  241. expect(calls).toEqual([])
  242. await nextTick()
  243. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  244. })
  245. it('should dedupe queued postFlushCb', async () => {
  246. const calls: string[] = []
  247. const cb1 = () => {
  248. calls.push('cb1')
  249. }
  250. const cb2 = () => {
  251. calls.push('cb2')
  252. }
  253. const cb3 = () => {
  254. calls.push('cb3')
  255. }
  256. queuePostFlushCb([cb1, cb2])
  257. queuePostFlushCb(cb3)
  258. queuePostFlushCb([cb1, cb3])
  259. queuePostFlushCb(cb2)
  260. expect(calls).toEqual([])
  261. await nextTick()
  262. expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
  263. })
  264. it('queuePostFlushCb while flushing', async () => {
  265. const calls: string[] = []
  266. const cb1 = () => {
  267. calls.push('cb1')
  268. // cb2 will be executed after cb1 at the same tick
  269. queuePostFlushCb(cb2)
  270. }
  271. const cb2 = () => {
  272. calls.push('cb2')
  273. }
  274. queuePostFlushCb(cb1)
  275. await nextTick()
  276. expect(calls).toEqual(['cb1', 'cb2'])
  277. })
  278. })
  279. describe('queueJob w/ queuePostFlushCb', () => {
  280. it('queueJob inside postFlushCb', async () => {
  281. const calls: string[] = []
  282. const job1 = () => {
  283. calls.push('job1')
  284. }
  285. const cb1 = () => {
  286. // queueJob in postFlushCb
  287. calls.push('cb1')
  288. queueJob(job1)
  289. }
  290. queuePostFlushCb(cb1)
  291. await nextTick()
  292. expect(calls).toEqual(['cb1', 'job1'])
  293. })
  294. it('queueJob & postFlushCb inside postFlushCb', async () => {
  295. const calls: string[] = []
  296. const job1 = () => {
  297. calls.push('job1')
  298. }
  299. const cb1 = () => {
  300. calls.push('cb1')
  301. queuePostFlushCb(cb2)
  302. // job1 will executed before cb2
  303. // Job has higher priority than postFlushCb
  304. queueJob(job1)
  305. }
  306. const cb2 = () => {
  307. calls.push('cb2')
  308. }
  309. queuePostFlushCb(cb1)
  310. await nextTick()
  311. expect(calls).toEqual(['cb1', 'job1', 'cb2'])
  312. })
  313. it('postFlushCb inside queueJob', async () => {
  314. const calls: string[] = []
  315. const job1 = () => {
  316. calls.push('job1')
  317. // postFlushCb in queueJob
  318. queuePostFlushCb(cb1)
  319. }
  320. const cb1 = () => {
  321. calls.push('cb1')
  322. }
  323. queueJob(job1)
  324. await nextTick()
  325. expect(calls).toEqual(['job1', 'cb1'])
  326. })
  327. it('queueJob & postFlushCb inside queueJob', async () => {
  328. const calls: string[] = []
  329. const job1 = () => {
  330. calls.push('job1')
  331. // cb1 will executed after job2
  332. // Job has higher priority than postFlushCb
  333. queuePostFlushCb(cb1)
  334. queueJob(job2)
  335. }
  336. const job2 = () => {
  337. calls.push('job2')
  338. }
  339. const cb1 = () => {
  340. calls.push('cb1')
  341. }
  342. queueJob(job1)
  343. await nextTick()
  344. expect(calls).toEqual(['job1', 'job2', 'cb1'])
  345. })
  346. it('nested queueJob w/ postFlushCb', async () => {
  347. const calls: string[] = []
  348. const job1 = () => {
  349. calls.push('job1')
  350. queuePostFlushCb(cb1)
  351. queueJob(job2)
  352. }
  353. const job2 = () => {
  354. calls.push('job2')
  355. queuePostFlushCb(cb2)
  356. }
  357. const cb1 = () => {
  358. calls.push('cb1')
  359. }
  360. const cb2 = () => {
  361. calls.push('cb2')
  362. }
  363. queueJob(job1)
  364. await nextTick()
  365. expect(calls).toEqual(['job1', 'job2', 'cb1', 'cb2'])
  366. })
  367. })
  368. test('invalidateJob', async () => {
  369. const calls: string[] = []
  370. const job1 = () => {
  371. calls.push('job1')
  372. invalidateJob(job2)
  373. job2()
  374. }
  375. const job2 = () => {
  376. calls.push('job2')
  377. }
  378. const job3 = () => {
  379. calls.push('job3')
  380. }
  381. const job4 = () => {
  382. calls.push('job4')
  383. }
  384. // queue all jobs
  385. queueJob(job1)
  386. queueJob(job2)
  387. queueJob(job3)
  388. queuePostFlushCb(job4)
  389. expect(calls).toEqual([])
  390. await nextTick()
  391. // job2 should be called only once
  392. expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
  393. })
  394. test('sort job based on id', async () => {
  395. const calls: string[] = []
  396. const job1 = () => calls.push('job1')
  397. // job1 has no id
  398. const job2 = () => calls.push('job2')
  399. job2.id = 2
  400. const job3 = () => calls.push('job3')
  401. job3.id = 1
  402. queueJob(job1)
  403. queueJob(job2)
  404. queueJob(job3)
  405. await nextTick()
  406. expect(calls).toEqual(['job3', 'job2', 'job1'])
  407. })
  408. test('sort SchedulerCbs based on id', async () => {
  409. const calls: string[] = []
  410. const cb1 = () => calls.push('cb1')
  411. // cb1 has no id
  412. const cb2 = () => calls.push('cb2')
  413. cb2.id = 2
  414. const cb3 = () => calls.push('cb3')
  415. cb3.id = 1
  416. queuePostFlushCb(cb1)
  417. queuePostFlushCb(cb2)
  418. queuePostFlushCb(cb3)
  419. await nextTick()
  420. expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
  421. })
  422. // #1595
  423. test('avoid duplicate postFlushCb invocation', async () => {
  424. const calls: string[] = []
  425. const cb1 = () => {
  426. calls.push('cb1')
  427. queuePostFlushCb(cb2)
  428. }
  429. const cb2 = () => {
  430. calls.push('cb2')
  431. }
  432. queuePostFlushCb(cb1)
  433. queuePostFlushCb(cb2)
  434. await nextTick()
  435. expect(calls).toEqual(['cb1', 'cb2'])
  436. })
  437. test('nextTick should capture scheduler flush errors', async () => {
  438. const err = new Error('test')
  439. queueJob(() => {
  440. throw err
  441. })
  442. try {
  443. await nextTick()
  444. } catch (e: any) {
  445. expect(e).toBe(err)
  446. }
  447. expect(
  448. `Unhandled error during execution of scheduler flush`,
  449. ).toHaveBeenWarned()
  450. // this one should no longer error
  451. await nextTick()
  452. })
  453. test('should prevent self-triggering jobs by default', async () => {
  454. let count = 0
  455. const job = () => {
  456. if (count < 3) {
  457. count++
  458. queueJob(job)
  459. }
  460. }
  461. queueJob(job)
  462. await nextTick()
  463. // only runs once - a job cannot queue itself
  464. expect(count).toBe(1)
  465. })
  466. test('should allow explicitly marked jobs to trigger itself', async () => {
  467. // normal job
  468. let count = 0
  469. const job: SchedulerJob = () => {
  470. if (count < 3) {
  471. count++
  472. queueJob(job)
  473. }
  474. }
  475. job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  476. queueJob(job)
  477. await nextTick()
  478. expect(count).toBe(3)
  479. // post cb
  480. const cb: SchedulerJob = () => {
  481. if (count < 5) {
  482. count++
  483. queuePostFlushCb(cb)
  484. }
  485. }
  486. cb.flags! |= SchedulerJobFlags.ALLOW_RECURSE
  487. queuePostFlushCb(cb)
  488. await nextTick()
  489. expect(count).toBe(5)
  490. })
  491. // #1947 flushPostFlushCbs should handle nested calls
  492. // e.g. app.mount inside app.mount
  493. test('flushPostFlushCbs', async () => {
  494. let count = 0
  495. const queueAndFlush = (hook: Function) => {
  496. queuePostFlushCb(hook)
  497. flushPostFlushCbs()
  498. }
  499. queueAndFlush(() => {
  500. queueAndFlush(() => {
  501. count++
  502. })
  503. })
  504. await nextTick()
  505. expect(count).toBe(1)
  506. })
  507. // #910
  508. test('should not run stopped reactive effects', async () => {
  509. const spy = vi.fn()
  510. // simulate parent component that toggles child
  511. const job1 = () => {
  512. // @ts-expect-error
  513. job2.flags! |= SchedulerJobFlags.DISPOSED
  514. }
  515. // simulate child that's triggered by the same reactive change that
  516. // triggers its toggle
  517. const job2 = () => spy()
  518. expect(spy).toHaveBeenCalledTimes(0)
  519. queueJob(job1)
  520. queueJob(job2)
  521. await nextTick()
  522. // should not be called
  523. expect(spy).toHaveBeenCalledTimes(0)
  524. })
  525. it('flushPreFlushCbs inside a pre job', async () => {
  526. const spy = vi.fn()
  527. const job: SchedulerJob = () => {
  528. spy()
  529. flushPreFlushCbs()
  530. }
  531. job.flags! |= SchedulerJobFlags.PRE
  532. queueJob(job)
  533. await nextTick()
  534. expect(spy).toHaveBeenCalledTimes(1)
  535. })
  536. it('nextTick should return promise', async () => {
  537. const fn = vi.fn(() => {
  538. return 1
  539. })
  540. const p = nextTick(fn)
  541. expect(p).toBeInstanceOf(Promise)
  542. expect(await p).toBe(1)
  543. expect(fn).toHaveBeenCalledTimes(1)
  544. })
  545. // #10003
  546. test('nested flushPostFlushCbs', async () => {
  547. const calls: string[] = []
  548. const cb1 = () => calls.push('cb1')
  549. // cb1 has no id
  550. const cb2 = () => calls.push('cb2')
  551. cb2.id = -1
  552. const queueAndFlush = (hook: Function) => {
  553. queuePostFlushCb(hook)
  554. flushPostFlushCbs()
  555. }
  556. queueAndFlush(() => {
  557. queuePostFlushCb([cb1, cb2])
  558. flushPostFlushCbs()
  559. })
  560. await nextTick()
  561. expect(calls).toEqual(['cb2', 'cb1'])
  562. })
  563. })