rendererOptimizedMode.spec.ts 23 KB

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