rendererOptimizedMode.spec.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937
  1. import { vi } from 'vitest'
  2. import {
  3. h,
  4. Fragment,
  5. Teleport,
  6. createVNode,
  7. createCommentVNode,
  8. openBlock,
  9. createBlock,
  10. render,
  11. nodeOps,
  12. TestElement,
  13. serialize,
  14. serializeInner as inner,
  15. VNode,
  16. ref,
  17. nextTick,
  18. defineComponent,
  19. withCtx,
  20. renderSlot,
  21. onBeforeUnmount,
  22. createTextVNode,
  23. SetupContext,
  24. createApp,
  25. FunctionalComponent,
  26. renderList,
  27. onUnmounted
  28. } from '@vue/runtime-test'
  29. import { PatchFlags, SlotFlags } from '@vue/shared'
  30. import { SuspenseImpl } from '../src/components/Suspense'
  31. describe('renderer: optimized mode', () => {
  32. let root: TestElement
  33. let block: VNode | null = null
  34. beforeEach(() => {
  35. root = nodeOps.createElement('div')
  36. block = null
  37. })
  38. const renderWithBlock = (renderChildren: () => VNode[]) => {
  39. render(
  40. (openBlock(), (block = createBlock('div', null, renderChildren()))),
  41. root
  42. )
  43. }
  44. test('basic use of block', () => {
  45. render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
  46. expect(block.dynamicChildren!.length).toBe(0)
  47. expect(inner(root)).toBe('<p>foo</p>')
  48. })
  49. test('block can appear anywhere in the vdom tree', () => {
  50. render(
  51. h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
  52. root
  53. )
  54. expect(block.dynamicChildren!.length).toBe(0)
  55. expect(inner(root)).toBe('<div><p>foo</p></div>')
  56. })
  57. test('block should collect dynamic vnodes', () => {
  58. renderWithBlock(() => [
  59. createVNode('p', null, 'foo', PatchFlags.TEXT),
  60. createVNode('i')
  61. ])
  62. expect(block!.dynamicChildren!.length).toBe(1)
  63. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  64. '<p>foo</p>'
  65. )
  66. })
  67. test('block can disable tracking', () => {
  68. render(
  69. // disable tracking
  70. (openBlock(true),
  71. (block = createBlock('div', null, [
  72. createVNode('p', null, 'foo', PatchFlags.TEXT)
  73. ]))),
  74. root
  75. )
  76. expect(block.dynamicChildren!.length).toBe(0)
  77. })
  78. test('block as dynamic children', () => {
  79. renderWithBlock(() => [
  80. (openBlock(), createBlock('div', { key: 0 }, [h('p')]))
  81. ])
  82. expect(block!.dynamicChildren!.length).toBe(1)
  83. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  84. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  85. '<div><p></p></div>'
  86. )
  87. renderWithBlock(() => [
  88. (openBlock(), createBlock('div', { key: 1 }, [h('i')]))
  89. ])
  90. expect(block!.dynamicChildren!.length).toBe(1)
  91. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  92. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  93. '<div><i></i></div>'
  94. )
  95. })
  96. test('PatchFlags: PatchFlags.TEXT', async () => {
  97. renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
  98. expect(inner(root)).toBe('<div><p>foo</p></div>')
  99. expect(block!.dynamicChildren!.length).toBe(1)
  100. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  101. '<p>foo</p>'
  102. )
  103. renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
  104. expect(inner(root)).toBe('<div><p>bar</p></div>')
  105. expect(block!.dynamicChildren!.length).toBe(1)
  106. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  107. '<p>bar</p>'
  108. )
  109. })
  110. test('PatchFlags: PatchFlags.CLASS', async () => {
  111. renderWithBlock(() => [
  112. createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS)
  113. ])
  114. expect(inner(root)).toBe('<div><p class="foo"></p></div>')
  115. expect(block!.dynamicChildren!.length).toBe(1)
  116. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  117. '<p class="foo"></p>'
  118. )
  119. renderWithBlock(() => [
  120. createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS)
  121. ])
  122. expect(inner(root)).toBe('<div><p class="bar"></p></div>')
  123. expect(block!.dynamicChildren!.length).toBe(1)
  124. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  125. '<p class="bar"></p>'
  126. )
  127. })
  128. test('PatchFlags: PatchFlags.STYLE', async () => {
  129. renderWithBlock(() => [
  130. createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE)
  131. ])
  132. expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
  133. expect(block!.dynamicChildren!.length).toBe(1)
  134. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  135. '<p style="color: red"></p>'
  136. )
  137. renderWithBlock(() => [
  138. createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE)
  139. ])
  140. expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
  141. expect(block!.dynamicChildren!.length).toBe(1)
  142. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  143. '<p style="color: green"></p>'
  144. )
  145. })
  146. test('PatchFlags: PatchFlags.PROPS', async () => {
  147. renderWithBlock(() => [
  148. createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id'])
  149. ])
  150. expect(inner(root)).toBe('<div><p id="foo"></p></div>')
  151. expect(block!.dynamicChildren!.length).toBe(1)
  152. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  153. '<p id="foo"></p>'
  154. )
  155. renderWithBlock(() => [
  156. createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id'])
  157. ])
  158. expect(inner(root)).toBe('<div><p id="bar"></p></div>')
  159. expect(block!.dynamicChildren!.length).toBe(1)
  160. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  161. '<p id="bar"></p>'
  162. )
  163. })
  164. test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
  165. let propName = 'foo'
  166. renderWithBlock(() => [
  167. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
  168. ])
  169. expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
  170. expect(block!.dynamicChildren!.length).toBe(1)
  171. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  172. '<p foo="dynamic"></p>'
  173. )
  174. propName = 'bar'
  175. renderWithBlock(() => [
  176. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
  177. ])
  178. expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
  179. expect(block!.dynamicChildren!.length).toBe(1)
  180. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  181. '<p bar="dynamic"></p>'
  182. )
  183. })
  184. // the order and length of the list will not change
  185. test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
  186. let list = ['foo', 'bar']
  187. render(
  188. (openBlock(),
  189. (block = createBlock(
  190. Fragment,
  191. null,
  192. list.map(item => {
  193. return createVNode('p', null, item, PatchFlags.TEXT)
  194. }),
  195. PatchFlags.STABLE_FRAGMENT
  196. ))),
  197. root
  198. )
  199. expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
  200. expect(block.dynamicChildren!.length).toBe(2)
  201. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  202. '<p>foo</p>'
  203. )
  204. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  205. '<p>bar</p>'
  206. )
  207. list = list.map(item => item.repeat(2))
  208. render(
  209. (openBlock(),
  210. createBlock(
  211. Fragment,
  212. null,
  213. list.map(item => {
  214. return createVNode('p', null, item, PatchFlags.TEXT)
  215. }),
  216. PatchFlags.STABLE_FRAGMENT
  217. )),
  218. root
  219. )
  220. expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
  221. expect(block.dynamicChildren!.length).toBe(2)
  222. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  223. '<p>foofoo</p>'
  224. )
  225. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  226. '<p>barbar</p>'
  227. )
  228. })
  229. // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
  230. // so there's no need for tracking dynamicChildren.
  231. test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
  232. const list = [{ tag: 'p', text: 'foo' }]
  233. render(
  234. (openBlock(true),
  235. (block = createBlock(
  236. Fragment,
  237. null,
  238. list.map(item => {
  239. return createVNode(item.tag, null, item.text)
  240. }),
  241. PatchFlags.UNKEYED_FRAGMENT
  242. ))),
  243. root
  244. )
  245. expect(inner(root)).toBe('<p>foo</p>')
  246. expect(block.dynamicChildren!.length).toBe(0)
  247. list.unshift({ tag: 'i', text: 'bar' })
  248. render(
  249. (openBlock(true),
  250. createBlock(
  251. Fragment,
  252. null,
  253. list.map(item => {
  254. return createVNode(item.tag, null, item.text)
  255. }),
  256. PatchFlags.UNKEYED_FRAGMENT
  257. )),
  258. root
  259. )
  260. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  261. expect(block.dynamicChildren!.length).toBe(0)
  262. })
  263. // A Fragment with `KEYED_FRAGMENT` will always patch its children,
  264. // so there's no need for tracking dynamicChildren.
  265. test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
  266. const list = [{ tag: 'p', text: 'foo' }]
  267. render(
  268. (openBlock(true),
  269. (block = createBlock(
  270. Fragment,
  271. null,
  272. list.map(item => {
  273. return createVNode(item.tag, { key: item.tag }, item.text)
  274. }),
  275. PatchFlags.KEYED_FRAGMENT
  276. ))),
  277. root
  278. )
  279. expect(inner(root)).toBe('<p>foo</p>')
  280. expect(block.dynamicChildren!.length).toBe(0)
  281. list.unshift({ tag: 'i', text: 'bar' })
  282. render(
  283. (openBlock(true),
  284. createBlock(
  285. Fragment,
  286. null,
  287. list.map(item => {
  288. return createVNode(item.tag, { key: item.tag }, item.text)
  289. }),
  290. PatchFlags.KEYED_FRAGMENT
  291. )),
  292. root
  293. )
  294. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  295. expect(block.dynamicChildren!.length).toBe(0)
  296. })
  297. test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
  298. const spyMounted = vi.fn()
  299. const spyUpdated = vi.fn()
  300. const count = ref(0)
  301. const Comp = {
  302. setup() {
  303. return () => {
  304. count.value
  305. return (
  306. openBlock(),
  307. (block = createBlock('div', null, [
  308. createVNode(
  309. 'p',
  310. { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
  311. '',
  312. PatchFlags.NEED_PATCH
  313. )
  314. ]))
  315. )
  316. }
  317. }
  318. }
  319. render(h(Comp), root)
  320. expect(inner(root)).toBe('<div><p></p></div>')
  321. expect(block!.dynamicChildren!.length).toBe(1)
  322. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  323. '<p></p>'
  324. )
  325. expect(spyMounted).toHaveBeenCalledTimes(1)
  326. expect(spyUpdated).toHaveBeenCalledTimes(0)
  327. count.value++
  328. await nextTick()
  329. expect(inner(root)).toBe('<div><p></p></div>')
  330. expect(block!.dynamicChildren!.length).toBe(1)
  331. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  332. '<p></p>'
  333. )
  334. expect(spyMounted).toHaveBeenCalledTimes(1)
  335. expect(spyUpdated).toHaveBeenCalledTimes(1)
  336. })
  337. test('PatchFlags: PatchFlags.BAIL', async () => {
  338. render(
  339. (openBlock(),
  340. (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
  341. root
  342. )
  343. expect(inner(root)).toBe('<div><p>foo</p></div>')
  344. expect(block!.dynamicChildren!.length).toBe(0)
  345. render(
  346. (openBlock(),
  347. (block = createBlock(
  348. 'div',
  349. null,
  350. [createVNode('i', null, 'bar')],
  351. PatchFlags.BAIL
  352. ))),
  353. root
  354. )
  355. expect(inner(root)).toBe('<div><i>bar</i></div>')
  356. expect(block!.dynamicChildren).toBe(null)
  357. })
  358. // #1980
  359. test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
  360. let block: VNode
  361. const Comp = defineComponent({
  362. setup(_props, { slots }) {
  363. return () => {
  364. const vnode =
  365. (openBlock(),
  366. (block = createBlock('div', null, {
  367. default: withCtx(() => [renderSlot(slots, 'default')]),
  368. _: SlotFlags.FORWARDED
  369. })))
  370. return vnode
  371. }
  372. }
  373. })
  374. const foo = ref(0)
  375. const App = {
  376. setup() {
  377. return () => {
  378. return createVNode(Comp, null, {
  379. default: withCtx(() => [
  380. createVNode('p', null, foo.value, PatchFlags.TEXT)
  381. ]),
  382. // Indicates that this is a stable slot to avoid bail out
  383. _: SlotFlags.STABLE
  384. })
  385. }
  386. }
  387. }
  388. render(h(App), root)
  389. expect(inner(root)).toBe('<div><p>0</p></div>')
  390. expect(block!.dynamicChildren!.length).toBe(1)
  391. expect(block!.dynamicChildren![0].type).toBe(Fragment)
  392. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  393. expect(
  394. serialize(
  395. block!.dynamicChildren![0].dynamicChildren![0].el as TestElement
  396. )
  397. ).toBe('<p>0</p>')
  398. foo.value++
  399. await nextTick()
  400. expect(inner(root)).toBe('<div><p>1</p></div>')
  401. })
  402. // #2169
  403. // block
  404. // - dynamic child (1)
  405. // - component (2)
  406. // When unmounting (1), we know we are in optimized mode so no need to further
  407. // traverse unmount its children
  408. test('should not perform unnecessary unmount traversals', () => {
  409. const spy = vi.fn()
  410. const Child = {
  411. setup() {
  412. onBeforeUnmount(spy)
  413. return () => 'child'
  414. }
  415. }
  416. const Parent = () => (
  417. openBlock(),
  418. createBlock('div', null, [
  419. createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */)
  420. ])
  421. )
  422. render(h(Parent), root)
  423. render(null, root)
  424. expect(spy).toHaveBeenCalledTimes(1)
  425. })
  426. // #2444
  427. // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
  428. test('non-stable Fragment always need to diff its children', () => {
  429. const spyA = vi.fn()
  430. const spyB = vi.fn()
  431. const ChildA = {
  432. setup() {
  433. onBeforeUnmount(spyA)
  434. return () => 'child'
  435. }
  436. }
  437. const ChildB = {
  438. setup() {
  439. onBeforeUnmount(spyB)
  440. return () => 'child'
  441. }
  442. }
  443. const Parent = () => (
  444. openBlock(),
  445. createBlock('div', null, [
  446. (openBlock(true),
  447. createBlock(
  448. Fragment,
  449. null,
  450. [createVNode(ChildA, { key: 0 })],
  451. 128 /* KEYED_FRAGMENT */
  452. )),
  453. (openBlock(true),
  454. createBlock(
  455. Fragment,
  456. null,
  457. [createVNode(ChildB)],
  458. 256 /* UNKEYED_FRAGMENT */
  459. ))
  460. ])
  461. )
  462. render(h(Parent), root)
  463. render(null, root)
  464. expect(spyA).toHaveBeenCalledTimes(1)
  465. expect(spyB).toHaveBeenCalledTimes(1)
  466. })
  467. // #2893
  468. test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
  469. const state = ref(0)
  470. const CompA = {
  471. setup(props: any, { slots }: SetupContext) {
  472. return () => {
  473. return (
  474. openBlock(),
  475. createBlock('div', null, [renderSlot(slots, 'default')])
  476. )
  477. }
  478. }
  479. }
  480. const Wrapper = {
  481. setup(props: any, { slots }: SetupContext) {
  482. // use the manually written render function to rendering the optimized slots,
  483. // which should make subsequent updates exit the optimized mode correctly
  484. return () => {
  485. return slots.default!()[state.value]
  486. }
  487. }
  488. }
  489. const app = createApp({
  490. setup() {
  491. return () => {
  492. return (
  493. openBlock(),
  494. createBlock(Wrapper, null, {
  495. default: withCtx(() => [
  496. createVNode(CompA, null, {
  497. default: withCtx(() => [createTextVNode('Hello')]),
  498. _: 1 /* STABLE */
  499. }),
  500. createVNode(CompA, null, {
  501. default: withCtx(() => [createTextVNode('World')]),
  502. _: 1 /* STABLE */
  503. })
  504. ]),
  505. _: 1 /* STABLE */
  506. })
  507. )
  508. }
  509. }
  510. })
  511. app.mount(root)
  512. expect(inner(root)).toBe('<div>Hello</div>')
  513. state.value = 1
  514. await nextTick()
  515. expect(inner(root)).toBe('<div>World</div>')
  516. })
  517. //#3623
  518. test('nested teleport unmount need exit the optimization mode', () => {
  519. const target = nodeOps.createElement('div')
  520. const root = nodeOps.createElement('div')
  521. render(
  522. (openBlock(),
  523. createBlock('div', null, [
  524. (openBlock(),
  525. createBlock(
  526. Teleport as any,
  527. {
  528. to: target
  529. },
  530. [
  531. createVNode('div', null, [
  532. (openBlock(),
  533. createBlock(
  534. Teleport as any,
  535. {
  536. to: target
  537. },
  538. [createVNode('div', null, 'foo')]
  539. ))
  540. ])
  541. ]
  542. ))
  543. ])),
  544. root
  545. )
  546. expect(inner(target)).toMatchInlineSnapshot(
  547. `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`
  548. )
  549. expect(inner(root)).toMatchInlineSnapshot(
  550. `"<div><!--teleport start--><!--teleport end--></div>"`
  551. )
  552. render(null, root)
  553. expect(inner(target)).toBe('')
  554. })
  555. // #3548
  556. test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
  557. const Comp = {
  558. setup(props: any, { slots }: SetupContext) {
  559. return () => {
  560. return (
  561. openBlock(),
  562. (block = createBlock('section', null, [
  563. renderSlot(slots, 'default')
  564. ]))
  565. )
  566. }
  567. }
  568. }
  569. let dynamicVNode: VNode
  570. const Wrapper = {
  571. setup(props: any, { slots }: SetupContext) {
  572. return () => {
  573. return (
  574. openBlock(),
  575. createBlock(Comp, null, {
  576. default: withCtx(() => {
  577. return [
  578. (dynamicVNode = createVNode(
  579. 'div',
  580. {
  581. class: {
  582. foo: !!slots.default!()
  583. }
  584. },
  585. null,
  586. PatchFlags.CLASS
  587. ))
  588. ]
  589. }),
  590. _: 1
  591. })
  592. )
  593. }
  594. }
  595. }
  596. const app = createApp({
  597. render() {
  598. return (
  599. openBlock(),
  600. createBlock(Wrapper, null, {
  601. default: withCtx(() => {
  602. return [createVNode({}) /* component */]
  603. }),
  604. _: 1
  605. })
  606. )
  607. }
  608. })
  609. app.mount(root)
  610. expect(inner(root)).toBe('<section><div class="foo"></div></section>')
  611. /**
  612. * Block Tree:
  613. * - block(div)
  614. * - block(Fragment): renderSlots()
  615. * - dynamicVNode
  616. */
  617. expect(block!.dynamicChildren!.length).toBe(1)
  618. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  619. expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
  620. dynamicVNode!
  621. )
  622. })
  623. // 3569
  624. test('should force bailout when the user manually calls the slot function', async () => {
  625. const index = ref(0)
  626. const Foo = {
  627. setup(props: any, { slots }: SetupContext) {
  628. return () => {
  629. return slots.default!()[index.value]
  630. }
  631. }
  632. }
  633. const app = createApp({
  634. setup() {
  635. return () => {
  636. return (
  637. openBlock(),
  638. createBlock(Foo, null, {
  639. default: withCtx(() => [
  640. true
  641. ? (openBlock(), createBlock('p', { key: 0 }, '1'))
  642. : createCommentVNode('v-if', true),
  643. true
  644. ? (openBlock(), createBlock('p', { key: 0 }, '2'))
  645. : createCommentVNode('v-if', true)
  646. ]),
  647. _: 1 /* STABLE */
  648. })
  649. )
  650. }
  651. }
  652. })
  653. app.mount(root)
  654. expect(inner(root)).toBe('<p>1</p>')
  655. index.value = 1
  656. await nextTick()
  657. expect(inner(root)).toBe('<p>2</p>')
  658. index.value = 0
  659. await nextTick()
  660. expect(inner(root)).toBe('<p>1</p>')
  661. })
  662. // #3779
  663. test('treat slots manually written by the user as dynamic', async () => {
  664. const Middle = {
  665. setup(props: any, { slots }: any) {
  666. return slots.default!
  667. }
  668. }
  669. const Comp = {
  670. setup(props: any, { slots }: any) {
  671. return () => {
  672. return (
  673. openBlock(),
  674. createBlock('div', null, [
  675. createVNode(Middle, null, {
  676. default: withCtx(
  677. () => [
  678. createVNode('div', null, [renderSlot(slots, 'default')])
  679. ],
  680. undefined
  681. ),
  682. _: 3 /* FORWARDED */
  683. })
  684. ])
  685. )
  686. }
  687. }
  688. }
  689. const loading = ref(false)
  690. const app = createApp({
  691. setup() {
  692. return () => {
  693. // important: write the slot content here
  694. const content = h('span', loading.value ? 'loading' : 'loaded')
  695. return h(Comp, null, {
  696. default: () => content
  697. })
  698. }
  699. }
  700. })
  701. app.mount(root)
  702. expect(inner(root)).toBe('<div><div><span>loaded</span></div></div>')
  703. loading.value = true
  704. await nextTick()
  705. expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
  706. })
  707. // #3828
  708. test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => {
  709. const show = ref(false)
  710. const app = createApp({
  711. render() {
  712. return (
  713. openBlock(),
  714. createBlock(
  715. Fragment,
  716. null,
  717. [
  718. (openBlock(),
  719. createBlock(SuspenseImpl, null, {
  720. default: withCtx(() => [
  721. createVNode('div', null, [
  722. createVNode('div', null, show.value, PatchFlags.TEXT)
  723. ])
  724. ]),
  725. _: SlotFlags.STABLE
  726. }))
  727. ],
  728. PatchFlags.STABLE_FRAGMENT
  729. )
  730. )
  731. }
  732. })
  733. app.mount(root)
  734. expect(inner(root)).toBe('<div><div>false</div></div>')
  735. show.value = true
  736. await nextTick()
  737. expect(inner(root)).toBe('<div><div>true</div></div>')
  738. })
  739. // #4183
  740. test('should not take unmount children fast path /w Suspense', async () => {
  741. const show = ref(true)
  742. const spyUnmounted = vi.fn()
  743. const Parent = {
  744. setup(props: any, { slots }: SetupContext) {
  745. return () => (
  746. openBlock(),
  747. createBlock(SuspenseImpl, null, {
  748. default: withCtx(() => [renderSlot(slots, 'default')]),
  749. _: SlotFlags.FORWARDED
  750. })
  751. )
  752. }
  753. }
  754. const Child = {
  755. setup() {
  756. onUnmounted(spyUnmounted)
  757. return () => createVNode('div', null, show.value, PatchFlags.TEXT)
  758. }
  759. }
  760. const app = createApp({
  761. render() {
  762. return show.value
  763. ? (openBlock(),
  764. createBlock(
  765. Parent,
  766. { key: 0 },
  767. {
  768. default: withCtx(() => [createVNode(Child)]),
  769. _: SlotFlags.STABLE
  770. }
  771. ))
  772. : createCommentVNode('v-if', true)
  773. }
  774. })
  775. app.mount(root)
  776. expect(inner(root)).toBe('<div>true</div>')
  777. show.value = false
  778. await nextTick()
  779. expect(inner(root)).toBe('<!--v-if-->')
  780. expect(spyUnmounted).toHaveBeenCalledTimes(1)
  781. })
  782. // #3881
  783. // root cause: fragment inside a compiled slot passed to component which
  784. // programmatically invokes the slot. The entire slot should de-opt but
  785. // the fragment was incorrectly put in optimized mode which causes it to skip
  786. // updates for its inner components.
  787. test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
  788. const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
  789. const Dummy = () => 'dummy'
  790. const toggle = ref(true)
  791. const force = ref(0)
  792. const app = createApp({
  793. render() {
  794. if (!toggle.value) {
  795. return null
  796. }
  797. return h(
  798. Parent,
  799. { n: force.value },
  800. {
  801. default: withCtx(
  802. () => [
  803. createVNode('ul', null, [
  804. (openBlock(),
  805. createBlock(
  806. Fragment,
  807. null,
  808. renderList(1, item => {
  809. return createVNode('li', null, [createVNode(Dummy)])
  810. }),
  811. 64 /* STABLE_FRAGMENT */
  812. ))
  813. ])
  814. ],
  815. undefined,
  816. true
  817. ),
  818. _: 1 /* STABLE */
  819. }
  820. )
  821. }
  822. })
  823. app.mount(root)
  824. // force a patch
  825. force.value++
  826. await nextTick()
  827. expect(inner(root)).toBe(`<ul><li>dummy</li></ul>`)
  828. // unmount
  829. toggle.value = false
  830. await nextTick()
  831. // should successfully unmount without error
  832. expect(inner(root)).toBe(`<!---->`)
  833. })
  834. })