scheduler.spec.ts 15 KB

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