rendererOptimizedMode.spec.ts 24 KB

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