rendererOptimizedMode.spec.ts 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. import {
  2. Fragment,
  3. type FunctionalComponent,
  4. type SetupContext,
  5. Teleport,
  6. type TestElement,
  7. type VNode,
  8. createApp,
  9. createBlock,
  10. createCommentVNode,
  11. createElementBlock,
  12. createElementVNode,
  13. createTextVNode,
  14. createVNode,
  15. defineComponent,
  16. h,
  17. serializeInner as inner,
  18. nextTick,
  19. nodeOps,
  20. onBeforeMount,
  21. onBeforeUnmount,
  22. onUnmounted,
  23. openBlock,
  24. ref,
  25. render,
  26. renderList,
  27. renderSlot,
  28. serialize,
  29. setBlockTracking,
  30. withCtx,
  31. } from '@vue/runtime-test'
  32. import { PatchFlags, SlotFlags, toDisplayString } from '@vue/shared'
  33. import { SuspenseImpl } from '../src/components/Suspense'
  34. describe('renderer: optimized mode', () => {
  35. let root: TestElement
  36. let block: VNode | null = null
  37. beforeEach(() => {
  38. root = nodeOps.createElement('div')
  39. block = null
  40. })
  41. const renderWithBlock = (renderChildren: () => VNode[]) => {
  42. render(
  43. (openBlock(), (block = createBlock('div', null, renderChildren()))),
  44. root,
  45. )
  46. }
  47. test('basic use of block', () => {
  48. render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
  49. expect(block.dynamicChildren!.length).toBe(0)
  50. expect(inner(root)).toBe('<p>foo</p>')
  51. })
  52. test('block can appear anywhere in the vdom tree', () => {
  53. render(
  54. h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
  55. root,
  56. )
  57. expect(block.dynamicChildren!.length).toBe(0)
  58. expect(inner(root)).toBe('<div><p>foo</p></div>')
  59. })
  60. test('block should collect dynamic vnodes', () => {
  61. renderWithBlock(() => [
  62. createVNode('p', null, 'foo', PatchFlags.TEXT),
  63. createVNode('i'),
  64. ])
  65. expect(block!.dynamicChildren!.length).toBe(1)
  66. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  67. '<p>foo</p>',
  68. )
  69. })
  70. test('block can disable tracking', () => {
  71. render(
  72. // disable tracking
  73. (openBlock(true),
  74. (block = createBlock('div', null, [
  75. createVNode('p', null, 'foo', PatchFlags.TEXT),
  76. ]))),
  77. root,
  78. )
  79. expect(block.dynamicChildren!.length).toBe(0)
  80. })
  81. test('block as dynamic children', () => {
  82. renderWithBlock(() => [
  83. (openBlock(), createBlock('div', { key: 0 }, [h('p')])),
  84. ])
  85. expect(block!.dynamicChildren!.length).toBe(1)
  86. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  87. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  88. '<div><p></p></div>',
  89. )
  90. renderWithBlock(() => [
  91. (openBlock(), createBlock('div', { key: 1 }, [h('i')])),
  92. ])
  93. expect(block!.dynamicChildren!.length).toBe(1)
  94. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  95. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  96. '<div><i></i></div>',
  97. )
  98. })
  99. test('PatchFlags: PatchFlags.TEXT', async () => {
  100. renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
  101. expect(inner(root)).toBe('<div><p>foo</p></div>')
  102. expect(block!.dynamicChildren!.length).toBe(1)
  103. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  104. '<p>foo</p>',
  105. )
  106. renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
  107. expect(inner(root)).toBe('<div><p>bar</p></div>')
  108. expect(block!.dynamicChildren!.length).toBe(1)
  109. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  110. '<p>bar</p>',
  111. )
  112. })
  113. test('PatchFlags: PatchFlags.CLASS', async () => {
  114. renderWithBlock(() => [
  115. createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS),
  116. ])
  117. expect(inner(root)).toBe('<div><p class="foo"></p></div>')
  118. expect(block!.dynamicChildren!.length).toBe(1)
  119. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  120. '<p class="foo"></p>',
  121. )
  122. renderWithBlock(() => [
  123. createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS),
  124. ])
  125. expect(inner(root)).toBe('<div><p class="bar"></p></div>')
  126. expect(block!.dynamicChildren!.length).toBe(1)
  127. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  128. '<p class="bar"></p>',
  129. )
  130. })
  131. test('PatchFlags: PatchFlags.STYLE', async () => {
  132. renderWithBlock(() => [
  133. createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE),
  134. ])
  135. expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
  136. expect(block!.dynamicChildren!.length).toBe(1)
  137. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  138. '<p style="color: red"></p>',
  139. )
  140. renderWithBlock(() => [
  141. createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE),
  142. ])
  143. expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
  144. expect(block!.dynamicChildren!.length).toBe(1)
  145. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  146. '<p style="color: green"></p>',
  147. )
  148. })
  149. test('PatchFlags: PatchFlags.PROPS', async () => {
  150. renderWithBlock(() => [
  151. createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id']),
  152. ])
  153. expect(inner(root)).toBe('<div><p id="foo"></p></div>')
  154. expect(block!.dynamicChildren!.length).toBe(1)
  155. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  156. '<p id="foo"></p>',
  157. )
  158. renderWithBlock(() => [
  159. createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id']),
  160. ])
  161. expect(inner(root)).toBe('<div><p id="bar"></p></div>')
  162. expect(block!.dynamicChildren!.length).toBe(1)
  163. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  164. '<p id="bar"></p>',
  165. )
  166. })
  167. test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
  168. let propName = 'foo'
  169. renderWithBlock(() => [
  170. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
  171. ])
  172. expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
  173. expect(block!.dynamicChildren!.length).toBe(1)
  174. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  175. '<p foo="dynamic"></p>',
  176. )
  177. propName = 'bar'
  178. renderWithBlock(() => [
  179. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS),
  180. ])
  181. expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
  182. expect(block!.dynamicChildren!.length).toBe(1)
  183. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  184. '<p bar="dynamic"></p>',
  185. )
  186. })
  187. // the order and length of the list will not change
  188. test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
  189. let list = ['foo', 'bar']
  190. render(
  191. (openBlock(),
  192. (block = createBlock(
  193. Fragment,
  194. null,
  195. list.map(item => {
  196. return createVNode('p', null, item, PatchFlags.TEXT)
  197. }),
  198. PatchFlags.STABLE_FRAGMENT,
  199. ))),
  200. root,
  201. )
  202. expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
  203. expect(block.dynamicChildren!.length).toBe(2)
  204. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  205. '<p>foo</p>',
  206. )
  207. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  208. '<p>bar</p>',
  209. )
  210. list = list.map(item => item.repeat(2))
  211. render(
  212. (openBlock(),
  213. createBlock(
  214. Fragment,
  215. null,
  216. list.map(item => {
  217. return createVNode('p', null, item, PatchFlags.TEXT)
  218. }),
  219. PatchFlags.STABLE_FRAGMENT,
  220. )),
  221. root,
  222. )
  223. expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
  224. expect(block.dynamicChildren!.length).toBe(2)
  225. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  226. '<p>foofoo</p>',
  227. )
  228. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  229. '<p>barbar</p>',
  230. )
  231. })
  232. // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
  233. // so there's no need for tracking dynamicChildren.
  234. test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
  235. const list = [{ tag: 'p', text: 'foo' }]
  236. render(
  237. (openBlock(true),
  238. (block = createBlock(
  239. Fragment,
  240. null,
  241. list.map(item => {
  242. return createVNode(item.tag, null, item.text)
  243. }),
  244. PatchFlags.UNKEYED_FRAGMENT,
  245. ))),
  246. root,
  247. )
  248. expect(inner(root)).toBe('<p>foo</p>')
  249. expect(block.dynamicChildren!.length).toBe(0)
  250. list.unshift({ tag: 'i', text: 'bar' })
  251. render(
  252. (openBlock(true),
  253. createBlock(
  254. Fragment,
  255. null,
  256. list.map(item => {
  257. return createVNode(item.tag, null, item.text)
  258. }),
  259. PatchFlags.UNKEYED_FRAGMENT,
  260. )),
  261. root,
  262. )
  263. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  264. expect(block.dynamicChildren!.length).toBe(0)
  265. })
  266. // A Fragment with `KEYED_FRAGMENT` will always patch its children,
  267. // so there's no need for tracking dynamicChildren.
  268. test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
  269. const list = [{ tag: 'p', text: 'foo' }]
  270. render(
  271. (openBlock(true),
  272. (block = createBlock(
  273. Fragment,
  274. null,
  275. list.map(item => {
  276. return createVNode(item.tag, { key: item.tag }, item.text)
  277. }),
  278. PatchFlags.KEYED_FRAGMENT,
  279. ))),
  280. root,
  281. )
  282. expect(inner(root)).toBe('<p>foo</p>')
  283. expect(block.dynamicChildren!.length).toBe(0)
  284. list.unshift({ tag: 'i', text: 'bar' })
  285. render(
  286. (openBlock(true),
  287. createBlock(
  288. Fragment,
  289. null,
  290. list.map(item => {
  291. return createVNode(item.tag, { key: item.tag }, item.text)
  292. }),
  293. PatchFlags.KEYED_FRAGMENT,
  294. )),
  295. root,
  296. )
  297. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  298. expect(block.dynamicChildren!.length).toBe(0)
  299. })
  300. test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
  301. const spyMounted = vi.fn()
  302. const spyUpdated = vi.fn()
  303. const count = ref(0)
  304. const Comp = {
  305. setup() {
  306. return () => {
  307. count.value
  308. return (
  309. openBlock(),
  310. (block = createBlock('div', null, [
  311. createVNode(
  312. 'p',
  313. { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
  314. '',
  315. PatchFlags.NEED_PATCH,
  316. ),
  317. ]))
  318. )
  319. }
  320. },
  321. }
  322. render(h(Comp), root)
  323. expect(inner(root)).toBe('<div><p></p></div>')
  324. expect(block!.dynamicChildren!.length).toBe(1)
  325. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  326. '<p></p>',
  327. )
  328. expect(spyMounted).toHaveBeenCalledTimes(1)
  329. expect(spyUpdated).toHaveBeenCalledTimes(0)
  330. count.value++
  331. await nextTick()
  332. expect(inner(root)).toBe('<div><p></p></div>')
  333. expect(block!.dynamicChildren!.length).toBe(1)
  334. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  335. '<p></p>',
  336. )
  337. expect(spyMounted).toHaveBeenCalledTimes(1)
  338. expect(spyUpdated).toHaveBeenCalledTimes(1)
  339. })
  340. test('PatchFlags: PatchFlags.BAIL', async () => {
  341. render(
  342. (openBlock(),
  343. (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
  344. root,
  345. )
  346. expect(inner(root)).toBe('<div><p>foo</p></div>')
  347. expect(block!.dynamicChildren!.length).toBe(0)
  348. render(
  349. (openBlock(),
  350. (block = createBlock(
  351. 'div',
  352. null,
  353. [createVNode('i', null, 'bar')],
  354. PatchFlags.BAIL,
  355. ))),
  356. root,
  357. )
  358. expect(inner(root)).toBe('<div><i>bar</i></div>')
  359. expect(block!.dynamicChildren).toBe(null)
  360. })
  361. // #1980
  362. test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
  363. let block: VNode
  364. const Comp = defineComponent({
  365. setup(_props, { slots }) {
  366. return () => {
  367. const vnode =
  368. (openBlock(),
  369. (block = createBlock('div', null, {
  370. default: withCtx(() => [renderSlot(slots, 'default')]),
  371. _: SlotFlags.FORWARDED,
  372. })))
  373. return vnode
  374. }
  375. },
  376. })
  377. const foo = ref(0)
  378. const App = {
  379. setup() {
  380. return () => {
  381. return createBlock(Comp, null, {
  382. default: withCtx(() => [
  383. createVNode('p', null, foo.value, PatchFlags.TEXT),
  384. ]),
  385. // Indicates that this is a stable slot to avoid bail out
  386. _: SlotFlags.STABLE,
  387. })
  388. }
  389. },
  390. }
  391. render(h(App), root)
  392. expect(inner(root)).toBe('<div><p>0</p></div>')
  393. expect(block!.dynamicChildren!.length).toBe(1)
  394. expect(block!.dynamicChildren![0].type).toBe(Fragment)
  395. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  396. expect(
  397. serialize(
  398. block!.dynamicChildren![0].dynamicChildren![0].el as TestElement,
  399. ),
  400. ).toBe('<p>0</p>')
  401. foo.value++
  402. await nextTick()
  403. expect(inner(root)).toBe('<div><p>1</p></div>')
  404. })
  405. // #2169
  406. // block
  407. // - dynamic child (1)
  408. // - component (2)
  409. // When unmounting (1), we know we are in optimized mode so no need to further
  410. // traverse unmount its children
  411. test('should not perform unnecessary unmount traversals', () => {
  412. const spy = vi.fn()
  413. const Child = {
  414. setup() {
  415. onBeforeUnmount(spy)
  416. return () => 'child'
  417. },
  418. }
  419. const Parent = () => (
  420. openBlock(),
  421. createBlock('div', null, [
  422. createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */),
  423. ])
  424. )
  425. render(h(Parent), root)
  426. render(null, root)
  427. expect(spy).toHaveBeenCalledTimes(1)
  428. })
  429. test('should call onUnmounted hook for dynamic components receiving an existing vnode w/ component children', async () => {
  430. const spy = vi.fn()
  431. const show = ref(1)
  432. const Child = {
  433. setup() {
  434. onUnmounted(spy)
  435. return () => 'child'
  436. },
  437. }
  438. const foo = h('div', null, h(Child))
  439. const app = createApp({
  440. render() {
  441. return show.value
  442. ? (openBlock(),
  443. createBlock('div', null, [(openBlock(), createBlock(foo))]))
  444. : createCommentVNode('v-if', true)
  445. },
  446. })
  447. app.mount(root)
  448. show.value = 0
  449. await nextTick()
  450. expect(spy).toHaveBeenCalledTimes(1)
  451. })
  452. // #2444
  453. // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
  454. test('non-stable Fragment always need to diff its children', () => {
  455. const spyA = vi.fn()
  456. const spyB = vi.fn()
  457. const ChildA = {
  458. setup() {
  459. onBeforeUnmount(spyA)
  460. return () => 'child'
  461. },
  462. }
  463. const ChildB = {
  464. setup() {
  465. onBeforeUnmount(spyB)
  466. return () => 'child'
  467. },
  468. }
  469. const Parent = () => (
  470. openBlock(),
  471. createBlock('div', null, [
  472. (openBlock(true),
  473. createBlock(
  474. Fragment,
  475. null,
  476. [createVNode(ChildA, { key: 0 })],
  477. 128 /* KEYED_FRAGMENT */,
  478. )),
  479. (openBlock(true),
  480. createBlock(
  481. Fragment,
  482. null,
  483. [createVNode(ChildB)],
  484. 256 /* UNKEYED_FRAGMENT */,
  485. )),
  486. ])
  487. )
  488. render(h(Parent), root)
  489. render(null, root)
  490. expect(spyA).toHaveBeenCalledTimes(1)
  491. expect(spyB).toHaveBeenCalledTimes(1)
  492. })
  493. // #2893
  494. test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
  495. const state = ref(0)
  496. const CompA = {
  497. name: 'A',
  498. setup(props: any, { slots }: SetupContext) {
  499. return () => {
  500. return (
  501. openBlock(),
  502. createBlock('div', null, [renderSlot(slots, 'default')])
  503. )
  504. }
  505. },
  506. }
  507. const Wrapper = {
  508. name: 'Wrapper',
  509. setup(props: any, { slots }: SetupContext) {
  510. // use the manually written render function to rendering the optimized slots,
  511. // which should make subsequent updates exit the optimized mode correctly
  512. return () => {
  513. return slots.default!()[state.value]
  514. }
  515. },
  516. }
  517. const app = createApp({
  518. name: 'App',
  519. setup() {
  520. return () => {
  521. return (
  522. openBlock(),
  523. createBlock(Wrapper, null, {
  524. default: withCtx(() => [
  525. createVNode(CompA, null, {
  526. default: withCtx(() => [createTextVNode('Hello')]),
  527. _: 1 /* STABLE */,
  528. }),
  529. createVNode(CompA, null, {
  530. default: withCtx(() => [createTextVNode('World')]),
  531. _: 1 /* STABLE */,
  532. }),
  533. ]),
  534. _: 1 /* STABLE */,
  535. })
  536. )
  537. }
  538. },
  539. })
  540. app.mount(root)
  541. expect(inner(root)).toBe('<div>Hello</div>')
  542. state.value = 1
  543. await nextTick()
  544. expect(inner(root)).toBe('<div>World</div>')
  545. })
  546. //#3623
  547. test('nested teleport unmount need exit the optimization mode', async () => {
  548. const target = nodeOps.createElement('div')
  549. const root = nodeOps.createElement('div')
  550. render(
  551. (openBlock(),
  552. createBlock('div', null, [
  553. (openBlock(),
  554. createBlock(
  555. Teleport as any,
  556. {
  557. to: target,
  558. },
  559. [
  560. createVNode('div', null, [
  561. (openBlock(),
  562. createBlock(
  563. Teleport as any,
  564. {
  565. to: target,
  566. },
  567. [createVNode('div', null, 'foo')],
  568. )),
  569. ]),
  570. ],
  571. )),
  572. ])),
  573. root,
  574. )
  575. await nextTick()
  576. expect(inner(target)).toMatchInlineSnapshot(
  577. `"<div><!--teleport start--><!--teleport end--></div><div>foo</div>"`,
  578. )
  579. expect(inner(root)).toMatchInlineSnapshot(
  580. `"<div><!--teleport start--><!--teleport end--></div>"`,
  581. )
  582. render(null, root)
  583. expect(inner(target)).toBe('')
  584. })
  585. // #3548
  586. test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
  587. const Comp = {
  588. setup(props: any, { slots }: SetupContext) {
  589. return () => {
  590. return (
  591. openBlock(),
  592. (block = createBlock('section', null, [
  593. renderSlot(slots, 'default'),
  594. ]))
  595. )
  596. }
  597. },
  598. }
  599. let dynamicVNode: VNode
  600. const Wrapper = {
  601. setup(props: any, { slots }: SetupContext) {
  602. return () => {
  603. return (
  604. openBlock(),
  605. createBlock(Comp, null, {
  606. default: withCtx(() => {
  607. return [
  608. (dynamicVNode = createVNode(
  609. 'div',
  610. {
  611. class: {
  612. foo: !!slots.default!(),
  613. },
  614. },
  615. null,
  616. PatchFlags.CLASS,
  617. )),
  618. ]
  619. }),
  620. _: 1,
  621. })
  622. )
  623. }
  624. },
  625. }
  626. const app = createApp({
  627. render() {
  628. return (
  629. openBlock(),
  630. createBlock(Wrapper, null, {
  631. default: withCtx(() => {
  632. return [createVNode({}) /* component */]
  633. }),
  634. _: 1,
  635. })
  636. )
  637. },
  638. })
  639. app.mount(root)
  640. expect(inner(root)).toBe('<section><div class="foo"></div></section>')
  641. /**
  642. * Block Tree:
  643. * - block(div)
  644. * - block(Fragment): renderSlots()
  645. * - dynamicVNode
  646. */
  647. expect(block!.dynamicChildren!.length).toBe(1)
  648. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  649. expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
  650. dynamicVNode!,
  651. )
  652. })
  653. // 3569
  654. test('should force bailout when the user manually calls the slot function', async () => {
  655. const index = ref(0)
  656. const Foo = {
  657. setup(props: any, { slots }: SetupContext) {
  658. return () => {
  659. return slots.default!()[index.value]
  660. }
  661. },
  662. }
  663. const app = createApp({
  664. setup() {
  665. return () => {
  666. return (
  667. openBlock(),
  668. createBlock(Foo, null, {
  669. default: withCtx(() => [
  670. true
  671. ? (openBlock(), createBlock('p', { key: 0 }, '1'))
  672. : createCommentVNode('v-if', true),
  673. true
  674. ? (openBlock(), createBlock('p', { key: 0 }, '2'))
  675. : createCommentVNode('v-if', true),
  676. ]),
  677. _: 1 /* STABLE */,
  678. })
  679. )
  680. }
  681. },
  682. })
  683. app.mount(root)
  684. expect(inner(root)).toBe('<p>1</p>')
  685. index.value = 1
  686. await nextTick()
  687. expect(inner(root)).toBe('<p>2</p>')
  688. index.value = 0
  689. await nextTick()
  690. expect(inner(root)).toBe('<p>1</p>')
  691. })
  692. // #3779
  693. test('treat slots manually written by the user as dynamic', async () => {
  694. const Middle = {
  695. setup(props: any, { slots }: any) {
  696. return slots.default!
  697. },
  698. }
  699. const Comp = {
  700. setup(props: any, { slots }: any) {
  701. return () => {
  702. return (
  703. openBlock(),
  704. createBlock('div', null, [
  705. createVNode(Middle, null, {
  706. default: withCtx(
  707. () => [
  708. createVNode('div', null, [renderSlot(slots, 'default')]),
  709. ],
  710. undefined,
  711. ),
  712. _: 3 /* FORWARDED */,
  713. }),
  714. ])
  715. )
  716. }
  717. },
  718. }
  719. const loading = ref(false)
  720. const app = createApp({
  721. setup() {
  722. return () => {
  723. // important: write the slot content here
  724. const content = h('span', loading.value ? 'loading' : 'loaded')
  725. return h(Comp, null, {
  726. default: () => content,
  727. })
  728. }
  729. },
  730. })
  731. app.mount(root)
  732. expect(inner(root)).toBe('<div><div><span>loaded</span></div></div>')
  733. loading.value = true
  734. await nextTick()
  735. expect(inner(root)).toBe('<div><div><span>loading</span></div></div>')
  736. })
  737. // #3828
  738. test('patch Suspense in optimized mode w/ nested dynamic nodes', async () => {
  739. const show = ref(false)
  740. const app = createApp({
  741. render() {
  742. return (
  743. openBlock(),
  744. createBlock(
  745. Fragment,
  746. null,
  747. [
  748. (openBlock(),
  749. createBlock(SuspenseImpl, null, {
  750. default: withCtx(() => [
  751. createVNode('div', null, [
  752. createVNode('div', null, show.value, PatchFlags.TEXT),
  753. ]),
  754. ]),
  755. _: SlotFlags.STABLE,
  756. })),
  757. ],
  758. PatchFlags.STABLE_FRAGMENT,
  759. )
  760. )
  761. },
  762. })
  763. app.mount(root)
  764. expect(inner(root)).toBe('<div><div>false</div></div>')
  765. show.value = true
  766. await nextTick()
  767. expect(inner(root)).toBe('<div><div>true</div></div>')
  768. })
  769. // #13305
  770. test('patch Suspense nested in list nodes in optimized mode', async () => {
  771. const deps: Promise<any>[] = []
  772. const Item = {
  773. props: {
  774. someId: { type: Number, required: true },
  775. },
  776. async setup(props: any) {
  777. const p = new Promise(resolve => setTimeout(resolve, 1))
  778. deps.push(p)
  779. await p
  780. return () => (
  781. openBlock(),
  782. createElementBlock('li', null, [
  783. createElementVNode(
  784. 'p',
  785. null,
  786. String(props.someId),
  787. PatchFlags.TEXT,
  788. ),
  789. ])
  790. )
  791. },
  792. }
  793. const list = ref([1, 2, 3])
  794. const App = {
  795. setup() {
  796. return () => (
  797. openBlock(),
  798. createElementBlock(
  799. Fragment,
  800. null,
  801. [
  802. createElementVNode(
  803. 'p',
  804. null,
  805. JSON.stringify(list.value),
  806. PatchFlags.TEXT,
  807. ),
  808. createElementVNode('ol', null, [
  809. (openBlock(),
  810. createBlock(SuspenseImpl, null, {
  811. fallback: withCtx(() => [
  812. createElementVNode('li', null, 'Loading…'),
  813. ]),
  814. default: withCtx(() => [
  815. (openBlock(true),
  816. createElementBlock(
  817. Fragment,
  818. null,
  819. renderList(list.value, id => {
  820. return (
  821. openBlock(),
  822. createBlock(
  823. Item,
  824. {
  825. key: id,
  826. 'some-id': id,
  827. },
  828. null,
  829. PatchFlags.PROPS,
  830. ['some-id'],
  831. )
  832. )
  833. }),
  834. PatchFlags.KEYED_FRAGMENT,
  835. )),
  836. ]),
  837. _: 1 /* STABLE */,
  838. })),
  839. ]),
  840. ],
  841. PatchFlags.STABLE_FRAGMENT,
  842. )
  843. )
  844. },
  845. }
  846. const app = createApp(App)
  847. app.mount(root)
  848. expect(inner(root)).toBe(`<p>[1,2,3]</p>` + `<ol><li>Loading…</li></ol>`)
  849. await Promise.all(deps)
  850. await nextTick()
  851. expect(inner(root)).toBe(
  852. `<p>[1,2,3]</p>` +
  853. `<ol>` +
  854. `<li><p>1</p></li>` +
  855. `<li><p>2</p></li>` +
  856. `<li><p>3</p></li>` +
  857. `</ol>`,
  858. )
  859. list.value = [3, 1, 2]
  860. await nextTick()
  861. expect(inner(root)).toBe(
  862. `<p>[3,1,2]</p>` +
  863. `<ol>` +
  864. `<li><p>3</p></li>` +
  865. `<li><p>1</p></li>` +
  866. `<li><p>2</p></li>` +
  867. `</ol>`,
  868. )
  869. })
  870. // #4183
  871. test('should not take unmount children fast path /w Suspense', async () => {
  872. const show = ref(true)
  873. const spyUnmounted = vi.fn()
  874. const Parent = {
  875. setup(props: any, { slots }: SetupContext) {
  876. return () => (
  877. openBlock(),
  878. createBlock(SuspenseImpl, null, {
  879. default: withCtx(() => [renderSlot(slots, 'default')]),
  880. _: SlotFlags.FORWARDED,
  881. })
  882. )
  883. },
  884. }
  885. const Child = {
  886. setup() {
  887. onUnmounted(spyUnmounted)
  888. return () => createVNode('div', null, show.value, PatchFlags.TEXT)
  889. },
  890. }
  891. const app = createApp({
  892. render() {
  893. return show.value
  894. ? (openBlock(),
  895. createBlock(
  896. Parent,
  897. { key: 0 },
  898. {
  899. default: withCtx(() => [createVNode(Child)]),
  900. _: SlotFlags.STABLE,
  901. },
  902. ))
  903. : createCommentVNode('v-if', true)
  904. },
  905. })
  906. app.mount(root)
  907. expect(inner(root)).toBe('<div>true</div>')
  908. show.value = false
  909. await nextTick()
  910. expect(inner(root)).toBe('<!--v-if-->')
  911. expect(spyUnmounted).toHaveBeenCalledTimes(1)
  912. })
  913. // #3881
  914. // root cause: fragment inside a compiled slot passed to component which
  915. // programmatically invokes the slot. The entire slot should de-opt but
  916. // the fragment was incorrectly put in optimized mode which causes it to skip
  917. // updates for its inner components.
  918. test('fragments inside programmatically invoked compiled slot should de-opt properly', async () => {
  919. const Parent: FunctionalComponent = (_, { slots }) => slots.default!()
  920. const Dummy = () => 'dummy'
  921. const toggle = ref(true)
  922. const force = ref(0)
  923. const app = createApp({
  924. render() {
  925. if (!toggle.value) {
  926. return null
  927. }
  928. return h(
  929. Parent,
  930. { n: force.value },
  931. {
  932. default: withCtx(
  933. () => [
  934. createVNode('ul', null, [
  935. (openBlock(),
  936. createBlock(
  937. Fragment,
  938. null,
  939. renderList(1, item => {
  940. return createVNode('li', null, [createVNode(Dummy)])
  941. }),
  942. 64 /* STABLE_FRAGMENT */,
  943. )),
  944. ]),
  945. ],
  946. undefined,
  947. true,
  948. ),
  949. _: 1 /* STABLE */,
  950. },
  951. )
  952. },
  953. })
  954. app.mount(root)
  955. // force a patch
  956. force.value++
  957. await nextTick()
  958. expect(inner(root)).toBe(`<ul><li>dummy</li></ul>`)
  959. // unmount
  960. toggle.value = false
  961. await nextTick()
  962. // should successfully unmount without error
  963. expect(inner(root)).toBe(`<!---->`)
  964. })
  965. // #10870
  966. test('should bail manually rendered compiler slots for both mount and update', async () => {
  967. // only reproducible in prod
  968. __DEV__ = false
  969. function Outer(_: any, { slots }: any) {
  970. return slots.default()
  971. }
  972. const Mid = {
  973. render(ctx: any) {
  974. return (
  975. openBlock(),
  976. createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
  977. )
  978. },
  979. }
  980. const state1 = ref(true)
  981. const state2 = ref(true)
  982. const App = {
  983. render() {
  984. return (
  985. openBlock(),
  986. createBlock(Outer, null, {
  987. default: withCtx(() => [
  988. createVNode(
  989. Mid,
  990. { foo: state2.value },
  991. {
  992. default: withCtx(() => [
  993. createElementVNode('div', null, [
  994. createElementVNode('div', null, [
  995. state2.value
  996. ? (openBlock(),
  997. createElementBlock(
  998. 'div',
  999. {
  1000. key: 0,
  1001. id: 'if',
  1002. foo: state1.value,
  1003. },
  1004. null,
  1005. 8 /* PROPS */,
  1006. ['foo'],
  1007. ))
  1008. : createCommentVNode('v-if', true),
  1009. ]),
  1010. ]),
  1011. ]),
  1012. _: 1 /* STABLE */,
  1013. },
  1014. 8 /* PROPS */,
  1015. ['foo'],
  1016. ),
  1017. ]),
  1018. _: 1 /* STABLE */,
  1019. })
  1020. )
  1021. },
  1022. }
  1023. const app = createApp(App)
  1024. app.config.errorHandler = vi.fn()
  1025. try {
  1026. app.mount(root)
  1027. state1.value = false
  1028. await nextTick()
  1029. state2.value = false
  1030. await nextTick()
  1031. } finally {
  1032. __DEV__ = true
  1033. expect(app.config.errorHandler).not.toHaveBeenCalled()
  1034. }
  1035. })
  1036. // #11336
  1037. test('should bail manually rendered compiler slots for both mount and update (2)', async () => {
  1038. // only reproducible in prod
  1039. __DEV__ = false
  1040. const n = ref(0)
  1041. function Outer(_: any, { slots }: any) {
  1042. n.value // track
  1043. return slots.default()
  1044. }
  1045. const Mid = {
  1046. render(ctx: any) {
  1047. return (
  1048. openBlock(),
  1049. createElementBlock('div', null, [renderSlot(ctx.$slots, 'default')])
  1050. )
  1051. },
  1052. }
  1053. const show = ref(false)
  1054. const App = {
  1055. render() {
  1056. return (
  1057. openBlock(),
  1058. createBlock(Outer, null, {
  1059. default: withCtx(() => [
  1060. createVNode(Mid, null, {
  1061. default: withCtx(() => [
  1062. createElementVNode('div', null, [
  1063. show.value
  1064. ? (openBlock(),
  1065. createElementBlock('div', { key: 0 }, '1'))
  1066. : createCommentVNode('v-if', true),
  1067. createElementVNode('div', null, '2'),
  1068. createElementVNode('div', null, '3'),
  1069. ]),
  1070. createElementVNode('div', null, '4'),
  1071. ]),
  1072. _: 1 /* STABLE */,
  1073. }),
  1074. ]),
  1075. _: 1 /* STABLE */,
  1076. })
  1077. )
  1078. },
  1079. }
  1080. const app = createApp(App)
  1081. app.config.errorHandler = vi.fn()
  1082. try {
  1083. app.mount(root)
  1084. // force Outer update, which will assign new slots to Mid
  1085. // we want to make sure the compiled slot flag doesn't accidentally
  1086. // get assigned again
  1087. n.value++
  1088. await nextTick()
  1089. show.value = true
  1090. await nextTick()
  1091. } finally {
  1092. __DEV__ = true
  1093. expect(app.config.errorHandler).not.toHaveBeenCalled()
  1094. }
  1095. })
  1096. test('diff slot and slot fallback node', async () => {
  1097. const Comp = {
  1098. props: ['show'],
  1099. setup(props: any, { slots }: SetupContext) {
  1100. return () => {
  1101. return (
  1102. openBlock(),
  1103. createElementBlock('div', null, [
  1104. renderSlot(slots, 'default', { hide: !props.show }, () => [
  1105. (openBlock(),
  1106. (block = createElementBlock(
  1107. Fragment,
  1108. { key: 0 },
  1109. [createTextVNode('foo')],
  1110. PatchFlags.STABLE_FRAGMENT,
  1111. ))),
  1112. ]),
  1113. ])
  1114. )
  1115. }
  1116. },
  1117. }
  1118. const show = ref(true)
  1119. const app = createApp({
  1120. render() {
  1121. return (
  1122. openBlock(),
  1123. createBlock(
  1124. Comp,
  1125. { show: show.value },
  1126. {
  1127. default: withCtx(({ hide }: { hide: boolean }) => [
  1128. !hide
  1129. ? (openBlock(),
  1130. createElementBlock(
  1131. Fragment,
  1132. { key: 0 },
  1133. [
  1134. createCommentVNode('comment'),
  1135. createElementVNode(
  1136. 'div',
  1137. null,
  1138. 'bar',
  1139. PatchFlags.CACHED,
  1140. ),
  1141. ],
  1142. PatchFlags.STABLE_FRAGMENT,
  1143. ))
  1144. : createCommentVNode('v-if', true),
  1145. ]),
  1146. _: SlotFlags.STABLE,
  1147. },
  1148. PatchFlags.PROPS,
  1149. ['show'],
  1150. )
  1151. )
  1152. },
  1153. })
  1154. app.mount(root)
  1155. expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
  1156. expect(block).toBe(null)
  1157. show.value = false
  1158. await nextTick()
  1159. expect(inner(root)).toBe('<div>foo</div>')
  1160. show.value = true
  1161. await nextTick()
  1162. expect(inner(root)).toBe('<div><!--comment--><div>bar</div></div>')
  1163. })
  1164. test('should not take unmount children fast path if children contain cached nodes', async () => {
  1165. const show = ref(true)
  1166. const spyUnmounted = vi.fn()
  1167. const Child = {
  1168. setup() {
  1169. onUnmounted(spyUnmounted)
  1170. return () => createVNode('div', null, 'Child')
  1171. },
  1172. }
  1173. const app = createApp({
  1174. render(_: any, cache: any) {
  1175. return show.value
  1176. ? (openBlock(),
  1177. createBlock('div', null, [
  1178. createVNode('div', null, [
  1179. cache[0] ||
  1180. (setBlockTracking(-1, true),
  1181. ((cache[0] = createVNode('div', null, [
  1182. createVNode(Child),
  1183. ])).cacheIndex = 0),
  1184. setBlockTracking(1),
  1185. cache[0]),
  1186. ]),
  1187. ]))
  1188. : createCommentVNode('v-if', true)
  1189. },
  1190. })
  1191. app.mount(root)
  1192. expect(inner(root)).toBe(
  1193. '<div><div><div><div>Child</div></div></div></div>',
  1194. )
  1195. show.value = false
  1196. await nextTick()
  1197. expect(inner(root)).toBe('<!--v-if-->')
  1198. expect(spyUnmounted).toHaveBeenCalledTimes(1)
  1199. show.value = true
  1200. await nextTick()
  1201. expect(inner(root)).toBe(
  1202. '<div><div><div><div>Child</div></div></div></div>',
  1203. )
  1204. // should unmount again, this verifies previous cache was properly cleared
  1205. show.value = false
  1206. await nextTick()
  1207. expect(inner(root)).toBe('<!--v-if-->')
  1208. expect(spyUnmounted).toHaveBeenCalledTimes(2)
  1209. })
  1210. // #12371
  1211. test('unmount children when the user calls a compiled slot', async () => {
  1212. const beforeMountSpy = vi.fn()
  1213. const beforeUnmountSpy = vi.fn()
  1214. const Child = {
  1215. setup() {
  1216. onBeforeMount(beforeMountSpy)
  1217. onBeforeUnmount(beforeUnmountSpy)
  1218. return () => 'child'
  1219. },
  1220. }
  1221. const Wrapper = {
  1222. setup(_: any, { slots }: SetupContext) {
  1223. return () => (
  1224. openBlock(),
  1225. createElementBlock('section', null, [
  1226. (openBlock(),
  1227. createElementBlock('div', { key: 1 }, [
  1228. createTextVNode(slots.header!() ? 'foo' : 'bar', 1 /* TEXT */),
  1229. renderSlot(slots, 'content'),
  1230. ])),
  1231. ])
  1232. )
  1233. },
  1234. }
  1235. const show = ref(false)
  1236. const app = createApp({
  1237. render() {
  1238. return show.value
  1239. ? (openBlock(),
  1240. createBlock(Wrapper, null, {
  1241. header: withCtx(() => [createVNode({})]),
  1242. content: withCtx(() => [createVNode(Child)]),
  1243. _: 1,
  1244. }))
  1245. : createCommentVNode('v-if', true)
  1246. },
  1247. })
  1248. app.mount(root)
  1249. expect(inner(root)).toMatchInlineSnapshot(`"<!--v-if-->"`)
  1250. expect(beforeMountSpy).toHaveBeenCalledTimes(0)
  1251. expect(beforeUnmountSpy).toHaveBeenCalledTimes(0)
  1252. show.value = true
  1253. await nextTick()
  1254. expect(inner(root)).toMatchInlineSnapshot(
  1255. `"<section><div>foochild</div></section>"`,
  1256. )
  1257. expect(beforeMountSpy).toHaveBeenCalledTimes(1)
  1258. show.value = false
  1259. await nextTick()
  1260. expect(inner(root)).toBe('<!--v-if-->')
  1261. expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
  1262. })
  1263. // #12411
  1264. test('handle patch stable fragment with non-reactive v-for source', async () => {
  1265. const count = ref(0)
  1266. const foo: any = []
  1267. function updateFoo() {
  1268. for (let n = 0; n < 3; n++) {
  1269. foo[n] = n + 1 + '_foo'
  1270. }
  1271. }
  1272. const Comp = {
  1273. setup() {
  1274. return () => {
  1275. // <div>{{ count }}</div>
  1276. // <div v-for='item in foo'>{{ item }}</div>
  1277. return (
  1278. openBlock(),
  1279. createElementBlock(
  1280. Fragment,
  1281. null,
  1282. [
  1283. createElementVNode(
  1284. 'div',
  1285. null,
  1286. toDisplayString(count.value),
  1287. PatchFlags.TEXT,
  1288. ),
  1289. (openBlock(),
  1290. createElementBlock(
  1291. Fragment,
  1292. null,
  1293. renderList(foo, item => {
  1294. return createElementVNode(
  1295. 'div',
  1296. null,
  1297. toDisplayString(item),
  1298. PatchFlags.TEXT,
  1299. )
  1300. }),
  1301. PatchFlags.STABLE_FRAGMENT,
  1302. )),
  1303. ],
  1304. PatchFlags.STABLE_FRAGMENT,
  1305. )
  1306. )
  1307. }
  1308. },
  1309. }
  1310. render(h(Comp), root)
  1311. expect(inner(root)).toBe('<div>0</div>')
  1312. updateFoo()
  1313. count.value++
  1314. await nextTick()
  1315. expect(inner(root)).toBe(
  1316. '<div>1</div><div>1_foo</div><div>2_foo</div><div>3_foo</div>',
  1317. )
  1318. })
  1319. })