rendererOptimizedMode.spec.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  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 = (openBlock(),
  363. (block = createBlock('div', null, {
  364. default: withCtx(() => [renderSlot(slots, 'default')]),
  365. _: SlotFlags.FORWARDED
  366. })))
  367. return vnode
  368. }
  369. }
  370. })
  371. const foo = ref(0)
  372. const App = {
  373. setup() {
  374. return () => {
  375. return createVNode(Comp, null, {
  376. default: withCtx(() => [
  377. createVNode('p', null, foo.value, PatchFlags.TEXT)
  378. ]),
  379. // Indicates that this is a stable slot to avoid bail out
  380. _: SlotFlags.STABLE
  381. })
  382. }
  383. }
  384. }
  385. render(h(App), root)
  386. expect(inner(root)).toBe('<div><p>0</p></div>')
  387. expect(block!.dynamicChildren!.length).toBe(1)
  388. expect(block!.dynamicChildren![0].type).toBe(Fragment)
  389. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  390. expect(
  391. serialize(block!.dynamicChildren![0].dynamicChildren![0]
  392. .el as TestElement)
  393. ).toBe('<p>0</p>')
  394. foo.value++
  395. await nextTick()
  396. expect(inner(root)).toBe('<div><p>1</p></div>')
  397. })
  398. // #2169
  399. // block
  400. // - dynamic child (1)
  401. // - component (2)
  402. // When unmounting (1), we know we are in optimized mode so no need to further
  403. // traverse unmount its children
  404. test('should not perform unnecessary unmount traversals', () => {
  405. const spy = jest.fn()
  406. const Child = {
  407. setup() {
  408. onBeforeUnmount(spy)
  409. return () => 'child'
  410. }
  411. }
  412. const Parent = () => (
  413. openBlock(),
  414. createBlock('div', null, [
  415. createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */)
  416. ])
  417. )
  418. render(h(Parent), root)
  419. render(null, root)
  420. expect(spy).toHaveBeenCalledTimes(1)
  421. })
  422. // #2444
  423. // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
  424. test('non-stable Fragment always need to diff its children', () => {
  425. const spyA = jest.fn()
  426. const spyB = jest.fn()
  427. const ChildA = {
  428. setup() {
  429. onBeforeUnmount(spyA)
  430. return () => 'child'
  431. }
  432. }
  433. const ChildB = {
  434. setup() {
  435. onBeforeUnmount(spyB)
  436. return () => 'child'
  437. }
  438. }
  439. const Parent = () => (
  440. openBlock(),
  441. createBlock('div', null, [
  442. (openBlock(true),
  443. createBlock(
  444. Fragment,
  445. null,
  446. [createVNode(ChildA, { key: 0 })],
  447. 128 /* KEYED_FRAGMENT */
  448. )),
  449. (openBlock(true),
  450. createBlock(
  451. Fragment,
  452. null,
  453. [createVNode(ChildB)],
  454. 256 /* UNKEYED_FRAGMENT */
  455. ))
  456. ])
  457. )
  458. render(h(Parent), root)
  459. render(null, root)
  460. expect(spyA).toHaveBeenCalledTimes(1)
  461. expect(spyB).toHaveBeenCalledTimes(1)
  462. })
  463. // #2893
  464. test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
  465. const state = ref(0)
  466. const CompA = {
  467. setup(props: any, { slots }: SetupContext) {
  468. return () => {
  469. return (
  470. openBlock(),
  471. createBlock('div', null, [renderSlot(slots, 'default')])
  472. )
  473. }
  474. }
  475. }
  476. const Wrapper = {
  477. setup(props: any, { slots }: SetupContext) {
  478. // use the manually written render function to rendering the optimized slots,
  479. // which should make subsequent updates exit the optimized mode correctly
  480. return () => {
  481. return slots.default!()[state.value]
  482. }
  483. }
  484. }
  485. const app = createApp({
  486. setup() {
  487. return () => {
  488. return (
  489. openBlock(),
  490. createBlock(Wrapper, null, {
  491. default: withCtx(() => [
  492. createVNode(CompA, null, {
  493. default: withCtx(() => [createTextVNode('Hello')]),
  494. _: 1 /* STABLE */
  495. }),
  496. createVNode(CompA, null, {
  497. default: withCtx(() => [createTextVNode('World')]),
  498. _: 1 /* STABLE */
  499. })
  500. ]),
  501. _: 1 /* STABLE */
  502. })
  503. )
  504. }
  505. }
  506. })
  507. app.mount(root)
  508. expect(inner(root)).toBe('<div>Hello</div>')
  509. state.value = 1
  510. await nextTick()
  511. expect(inner(root)).toBe('<div>World</div>')
  512. })
  513. //#3623
  514. test('nested teleport unmount need exit the optimization mode', () => {
  515. const target = nodeOps.createElement('div')
  516. const root = nodeOps.createElement('div')
  517. render(
  518. (openBlock(),
  519. createBlock('div', null, [
  520. (openBlock(),
  521. createBlock(
  522. Teleport as any,
  523. {
  524. to: target
  525. },
  526. [
  527. createVNode('div', null, [
  528. (openBlock(),
  529. createBlock(
  530. Teleport as any,
  531. {
  532. to: target
  533. },
  534. [createVNode('div', null, 'foo')]
  535. ))
  536. ])
  537. ]
  538. ))
  539. ])),
  540. root
  541. )
  542. expect(inner(target)).toMatchInlineSnapshot(
  543. `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`
  544. )
  545. expect(inner(root)).toMatchInlineSnapshot(
  546. `"<div><!--teleport start--><!--teleport end--></div>"`
  547. )
  548. render(null, root)
  549. expect(inner(target)).toBe('')
  550. })
  551. // #3548
  552. test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
  553. const Comp = {
  554. setup(props: any, { slots }: SetupContext) {
  555. return () => {
  556. return (
  557. openBlock(),
  558. (block = createBlock('section', null, [
  559. renderSlot(slots, 'default')
  560. ]))
  561. )
  562. }
  563. }
  564. }
  565. let dynamicVNode: VNode
  566. const Wrapper = {
  567. setup(props: any, { slots }: SetupContext) {
  568. return () => {
  569. return (
  570. openBlock(),
  571. createBlock(Comp, null, {
  572. default: withCtx(() => {
  573. return [
  574. (dynamicVNode = createVNode(
  575. 'div',
  576. {
  577. class: {
  578. foo: !!slots.default!()
  579. }
  580. },
  581. null,
  582. PatchFlags.CLASS
  583. ))
  584. ]
  585. }),
  586. _: 1
  587. })
  588. )
  589. }
  590. }
  591. }
  592. const app = createApp({
  593. render() {
  594. return (
  595. openBlock(),
  596. createBlock(Wrapper, null, {
  597. default: withCtx(() => {
  598. return [createVNode({}) /* component */]
  599. }),
  600. _: 1
  601. })
  602. )
  603. }
  604. })
  605. app.mount(root)
  606. expect(inner(root)).toBe('<section><div class="foo"></div></section>')
  607. /**
  608. * Block Tree:
  609. * - block(div)
  610. * - block(Fragment): renderSlots()
  611. * - dynamicVNode
  612. */
  613. expect(block!.dynamicChildren!.length).toBe(1)
  614. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  615. expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
  616. dynamicVNode!
  617. )
  618. })
  619. // 3569
  620. test('should force bailout when the user manually calls the slot function', async () => {
  621. const index = ref(0)
  622. const Foo = {
  623. setup(props: any, { slots }: SetupContext) {
  624. return () => {
  625. return slots.default!()[index.value]
  626. }
  627. }
  628. }
  629. const app = createApp({
  630. setup() {
  631. return () => {
  632. return (
  633. openBlock(),
  634. createBlock(Foo, null, {
  635. default: withCtx(() => [
  636. true
  637. ? (openBlock(), createBlock('p', { key: 0 }, '1'))
  638. : createCommentVNode('v-if', true),
  639. true
  640. ? (openBlock(), createBlock('p', { key: 0 }, '2'))
  641. : createCommentVNode('v-if', true)
  642. ]),
  643. _: 1 /* STABLE */
  644. })
  645. )
  646. }
  647. }
  648. })
  649. app.mount(root)
  650. expect(inner(root)).toBe('<p>1</p>')
  651. index.value = 1
  652. await nextTick()
  653. expect(inner(root)).toBe('<p>2</p>')
  654. index.value = 0
  655. await nextTick()
  656. expect(inner(root)).toBe('<p>1</p>')
  657. })
  658. // #3779
  659. test('treat slots manually written by the user as dynamic', async () => {
  660. const Middle = {
  661. setup(props: any, { slots }: any) {
  662. return slots.default!
  663. }
  664. }
  665. const Comp = {
  666. setup(props: any, { slots }: any) {
  667. return () => {
  668. return (
  669. openBlock(),
  670. createBlock('div', null, [
  671. createVNode(Middle, null, {
  672. default: withCtx(
  673. () => [
  674. createVNode('div', null, [renderSlot(slots, 'default')])
  675. ],
  676. undefined
  677. ),
  678. _: 3 /* FORWARDED */
  679. })
  680. ])
  681. )
  682. }
  683. }
  684. }
  685. const loading = ref(false)
  686. const app = createApp({
  687. setup() {
  688. return () => {
  689. // important: write the slot content here
  690. const content = h('span', loading.value ? 'loading' : 'loaded')
  691. return h(Comp, null, {
  692. default: () => content
  693. })
  694. }
  695. }
  696. })
  697. app.mount(root)
  698. expect(inner(root)).toBe('<div><div><span>loaded</span></div></div>')
  699. loading.value = true
  700. await nextTick()
  701. expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
  702. })
  703. // #3828
  704. test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => {
  705. const show = ref(false)
  706. const app = createApp({
  707. render() {
  708. return (
  709. openBlock(),
  710. createBlock(
  711. Fragment,
  712. null,
  713. [
  714. (openBlock(),
  715. createBlock(SuspenseImpl, null, {
  716. default: withCtx(() => [
  717. createVNode('div', null, [
  718. createVNode('div', null, show.value, PatchFlags.TEXT)
  719. ])
  720. ]),
  721. _: SlotFlags.STABLE
  722. }))
  723. ],
  724. PatchFlags.STABLE_FRAGMENT
  725. )
  726. )
  727. }
  728. })
  729. app.mount(root)
  730. expect(inner(root)).toBe('<div><div>false</div></div>')
  731. show.value = true
  732. await nextTick()
  733. expect(inner(root)).toBe('<div><div>true</div></div>')
  734. })
  735. // #3881
  736. // root cause: fragment inside a compiled slot passed to component which
  737. // programmatically invokes the slot. The entire slot should de-opt but
  738. // the fragment was incorretly put in optimized mode which causes it to skip
  739. // updates for its inner components.
  740. test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
  741. const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
  742. const Dummy = () => 'dummy'
  743. const toggle = ref(true)
  744. const force = ref(0)
  745. const app = createApp({
  746. render() {
  747. if (!toggle.value) {
  748. return null
  749. }
  750. return h(
  751. Parent,
  752. { n: force.value },
  753. {
  754. default: withCtx(
  755. () => [
  756. createVNode('ul', null, [
  757. (openBlock(),
  758. createBlock(
  759. Fragment,
  760. null,
  761. renderList(1, item => {
  762. return createVNode('li', null, [createVNode(Dummy)])
  763. }),
  764. 64 /* STABLE_FRAGMENT */
  765. ))
  766. ])
  767. ],
  768. undefined,
  769. true
  770. ),
  771. _: 1 /* STABLE */
  772. }
  773. )
  774. }
  775. })
  776. app.mount(root)
  777. // force a patch
  778. force.value++
  779. await nextTick()
  780. expect(inner(root)).toBe(`<ul><li>dummy</li></ul>`)
  781. // unmount
  782. toggle.value = false
  783. await nextTick()
  784. // should successfully unmount without error
  785. expect(inner(root)).toBe(`<!---->`)
  786. })
  787. })