scheduler.spec.ts 13 KB

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