rendererOptimizedMode.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. import {
  2. h,
  3. Fragment,
  4. createVNode,
  5. createCommentVNode,
  6. openBlock,
  7. createBlock,
  8. render,
  9. nodeOps,
  10. TestElement,
  11. serialize,
  12. serializeInner as inner,
  13. VNode,
  14. ref,
  15. nextTick,
  16. defineComponent,
  17. withCtx,
  18. renderSlot,
  19. onBeforeUnmount,
  20. createTextVNode,
  21. SetupContext,
  22. createApp
  23. } from '@vue/runtime-test'
  24. import { PatchFlags, SlotFlags } from '@vue/shared'
  25. describe('renderer: optimized mode', () => {
  26. let root: TestElement
  27. let block: VNode | null = null
  28. beforeEach(() => {
  29. root = nodeOps.createElement('div')
  30. block = null
  31. })
  32. const renderWithBlock = (renderChildren: () => VNode[]) => {
  33. render(
  34. (openBlock(), (block = createBlock('div', null, renderChildren()))),
  35. root
  36. )
  37. }
  38. test('basic use of block', () => {
  39. render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
  40. expect(block.dynamicChildren!.length).toBe(0)
  41. expect(inner(root)).toBe('<p>foo</p>')
  42. })
  43. test('block can appear anywhere in the vdom tree', () => {
  44. render(
  45. h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
  46. root
  47. )
  48. expect(block.dynamicChildren!.length).toBe(0)
  49. expect(inner(root)).toBe('<div><p>foo</p></div>')
  50. })
  51. test('block should collect dynamic vnodes', () => {
  52. renderWithBlock(() => [
  53. createVNode('p', null, 'foo', PatchFlags.TEXT),
  54. createVNode('i')
  55. ])
  56. expect(block!.dynamicChildren!.length).toBe(1)
  57. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  58. '<p>foo</p>'
  59. )
  60. })
  61. test('block can disable tracking', () => {
  62. render(
  63. // disable tracking
  64. (openBlock(true),
  65. (block = createBlock('div', null, [
  66. createVNode('p', null, 'foo', PatchFlags.TEXT)
  67. ]))),
  68. root
  69. )
  70. expect(block.dynamicChildren!.length).toBe(0)
  71. })
  72. test('block as dynamic children', () => {
  73. renderWithBlock(() => [
  74. (openBlock(), createBlock('div', { key: 0 }, [h('p')]))
  75. ])
  76. expect(block!.dynamicChildren!.length).toBe(1)
  77. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  78. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  79. '<div><p></p></div>'
  80. )
  81. renderWithBlock(() => [
  82. (openBlock(), createBlock('div', { key: 1 }, [h('i')]))
  83. ])
  84. expect(block!.dynamicChildren!.length).toBe(1)
  85. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
  86. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  87. '<div><i></i></div>'
  88. )
  89. })
  90. test('PatchFlags: PatchFlags.TEXT', async () => {
  91. renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
  92. expect(inner(root)).toBe('<div><p>foo</p></div>')
  93. expect(block!.dynamicChildren!.length).toBe(1)
  94. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  95. '<p>foo</p>'
  96. )
  97. renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
  98. expect(inner(root)).toBe('<div><p>bar</p></div>')
  99. expect(block!.dynamicChildren!.length).toBe(1)
  100. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  101. '<p>bar</p>'
  102. )
  103. })
  104. test('PatchFlags: PatchFlags.CLASS', async () => {
  105. renderWithBlock(() => [
  106. createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS)
  107. ])
  108. expect(inner(root)).toBe('<div><p class="foo"></p></div>')
  109. expect(block!.dynamicChildren!.length).toBe(1)
  110. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  111. '<p class="foo"></p>'
  112. )
  113. renderWithBlock(() => [
  114. createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS)
  115. ])
  116. expect(inner(root)).toBe('<div><p class="bar"></p></div>')
  117. expect(block!.dynamicChildren!.length).toBe(1)
  118. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  119. '<p class="bar"></p>'
  120. )
  121. })
  122. test('PatchFlags: PatchFlags.STYLE', async () => {
  123. renderWithBlock(() => [
  124. createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE)
  125. ])
  126. expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
  127. expect(block!.dynamicChildren!.length).toBe(1)
  128. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  129. '<p style="color: red"></p>'
  130. )
  131. renderWithBlock(() => [
  132. createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE)
  133. ])
  134. expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
  135. expect(block!.dynamicChildren!.length).toBe(1)
  136. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  137. '<p style="color: green"></p>'
  138. )
  139. })
  140. test('PatchFlags: PatchFlags.PROPS', async () => {
  141. renderWithBlock(() => [
  142. createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id'])
  143. ])
  144. expect(inner(root)).toBe('<div><p id="foo"></p></div>')
  145. expect(block!.dynamicChildren!.length).toBe(1)
  146. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  147. '<p id="foo"></p>'
  148. )
  149. renderWithBlock(() => [
  150. createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id'])
  151. ])
  152. expect(inner(root)).toBe('<div><p id="bar"></p></div>')
  153. expect(block!.dynamicChildren!.length).toBe(1)
  154. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  155. '<p id="bar"></p>'
  156. )
  157. })
  158. test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
  159. let propName = 'foo'
  160. renderWithBlock(() => [
  161. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
  162. ])
  163. expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
  164. expect(block!.dynamicChildren!.length).toBe(1)
  165. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  166. '<p foo="dynamic"></p>'
  167. )
  168. propName = 'bar'
  169. renderWithBlock(() => [
  170. createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
  171. ])
  172. expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
  173. expect(block!.dynamicChildren!.length).toBe(1)
  174. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  175. '<p bar="dynamic"></p>'
  176. )
  177. })
  178. // the order and length of the list will not change
  179. test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
  180. let list = ['foo', 'bar']
  181. render(
  182. (openBlock(),
  183. (block = createBlock(
  184. Fragment,
  185. null,
  186. list.map(item => {
  187. return createVNode('p', null, item, PatchFlags.TEXT)
  188. }),
  189. PatchFlags.STABLE_FRAGMENT
  190. ))),
  191. root
  192. )
  193. expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
  194. expect(block.dynamicChildren!.length).toBe(2)
  195. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  196. '<p>foo</p>'
  197. )
  198. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  199. '<p>bar</p>'
  200. )
  201. list = list.map(item => item.repeat(2))
  202. render(
  203. (openBlock(),
  204. createBlock(
  205. Fragment,
  206. null,
  207. list.map(item => {
  208. return createVNode('p', null, item, PatchFlags.TEXT)
  209. }),
  210. PatchFlags.STABLE_FRAGMENT
  211. )),
  212. root
  213. )
  214. expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
  215. expect(block.dynamicChildren!.length).toBe(2)
  216. expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
  217. '<p>foofoo</p>'
  218. )
  219. expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
  220. '<p>barbar</p>'
  221. )
  222. })
  223. // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
  224. // so there's no need for tracking dynamicChildren.
  225. test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
  226. const list = [{ tag: 'p', text: 'foo' }]
  227. render(
  228. (openBlock(true),
  229. (block = createBlock(
  230. Fragment,
  231. null,
  232. list.map(item => {
  233. return createVNode(item.tag, null, item.text)
  234. }),
  235. PatchFlags.UNKEYED_FRAGMENT
  236. ))),
  237. root
  238. )
  239. expect(inner(root)).toBe('<p>foo</p>')
  240. expect(block.dynamicChildren!.length).toBe(0)
  241. list.unshift({ tag: 'i', text: 'bar' })
  242. render(
  243. (openBlock(true),
  244. createBlock(
  245. Fragment,
  246. null,
  247. list.map(item => {
  248. return createVNode(item.tag, null, item.text)
  249. }),
  250. PatchFlags.UNKEYED_FRAGMENT
  251. )),
  252. root
  253. )
  254. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  255. expect(block.dynamicChildren!.length).toBe(0)
  256. })
  257. // A Fragment with `KEYED_FRAGMENT` will always patch its children,
  258. // so there's no need for tracking dynamicChildren.
  259. test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
  260. const list = [{ tag: 'p', text: 'foo' }]
  261. render(
  262. (openBlock(true),
  263. (block = createBlock(
  264. Fragment,
  265. null,
  266. list.map(item => {
  267. return createVNode(item.tag, { key: item.tag }, item.text)
  268. }),
  269. PatchFlags.KEYED_FRAGMENT
  270. ))),
  271. root
  272. )
  273. expect(inner(root)).toBe('<p>foo</p>')
  274. expect(block.dynamicChildren!.length).toBe(0)
  275. list.unshift({ tag: 'i', text: 'bar' })
  276. render(
  277. (openBlock(true),
  278. createBlock(
  279. Fragment,
  280. null,
  281. list.map(item => {
  282. return createVNode(item.tag, { key: item.tag }, item.text)
  283. }),
  284. PatchFlags.KEYED_FRAGMENT
  285. )),
  286. root
  287. )
  288. expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
  289. expect(block.dynamicChildren!.length).toBe(0)
  290. })
  291. test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
  292. const spyMounted = jest.fn()
  293. const spyUpdated = jest.fn()
  294. const count = ref(0)
  295. const Comp = {
  296. setup() {
  297. return () => {
  298. count.value
  299. return (
  300. openBlock(),
  301. (block = createBlock('div', null, [
  302. createVNode(
  303. 'p',
  304. { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
  305. '',
  306. PatchFlags.NEED_PATCH
  307. )
  308. ]))
  309. )
  310. }
  311. }
  312. }
  313. render(h(Comp), root)
  314. expect(inner(root)).toBe('<div><p></p></div>')
  315. expect(block!.dynamicChildren!.length).toBe(1)
  316. expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
  317. '<p></p>'
  318. )
  319. expect(spyMounted).toHaveBeenCalledTimes(1)
  320. expect(spyUpdated).toHaveBeenCalledTimes(0)
  321. count.value++
  322. await nextTick()
  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(1)
  330. })
  331. test('PatchFlags: PatchFlags.BAIL', async () => {
  332. render(
  333. (openBlock(),
  334. (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
  335. root
  336. )
  337. expect(inner(root)).toBe('<div><p>foo</p></div>')
  338. expect(block!.dynamicChildren!.length).toBe(0)
  339. render(
  340. (openBlock(),
  341. (block = createBlock(
  342. 'div',
  343. null,
  344. [createVNode('i', null, 'bar')],
  345. PatchFlags.BAIL
  346. ))),
  347. root
  348. )
  349. expect(inner(root)).toBe('<div><i>bar</i></div>')
  350. expect(block!.dynamicChildren).toBe(null)
  351. })
  352. // #1980
  353. test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
  354. let block: VNode
  355. const Comp = defineComponent({
  356. setup(_props, { slots }) {
  357. return () => {
  358. const vnode = (openBlock(),
  359. (block = createBlock('div', null, {
  360. default: withCtx(() => [renderSlot(slots, 'default')]),
  361. _: SlotFlags.FORWARDED
  362. })))
  363. return vnode
  364. }
  365. }
  366. })
  367. const foo = ref(0)
  368. const App = {
  369. setup() {
  370. return () => {
  371. return createVNode(Comp, null, {
  372. default: withCtx(() => [
  373. createVNode('p', null, foo.value, PatchFlags.TEXT)
  374. ]),
  375. // Indicates that this is a stable slot to avoid bail out
  376. _: SlotFlags.STABLE
  377. })
  378. }
  379. }
  380. }
  381. render(h(App), root)
  382. expect(inner(root)).toBe('<div><p>0</p></div>')
  383. expect(block!.dynamicChildren!.length).toBe(1)
  384. expect(block!.dynamicChildren![0].type).toBe(Fragment)
  385. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  386. expect(
  387. serialize(block!.dynamicChildren![0].dynamicChildren![0]
  388. .el as TestElement)
  389. ).toBe('<p>0</p>')
  390. foo.value++
  391. await nextTick()
  392. expect(inner(root)).toBe('<div><p>1</p></div>')
  393. })
  394. // #2169
  395. // block
  396. // - dynamic child (1)
  397. // - component (2)
  398. // When unmounting (1), we know we are in optimized mode so no need to further
  399. // traverse unmount its children
  400. test('should not perform unnecessary unmount traversals', () => {
  401. const spy = jest.fn()
  402. const Child = {
  403. setup() {
  404. onBeforeUnmount(spy)
  405. return () => 'child'
  406. }
  407. }
  408. const Parent = () => (
  409. openBlock(),
  410. createBlock('div', null, [
  411. createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */)
  412. ])
  413. )
  414. render(h(Parent), root)
  415. render(null, root)
  416. expect(spy).toHaveBeenCalledTimes(1)
  417. })
  418. // #2444
  419. // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
  420. test('non-stable Fragment always need to diff its children', () => {
  421. const spyA = jest.fn()
  422. const spyB = jest.fn()
  423. const ChildA = {
  424. setup() {
  425. onBeforeUnmount(spyA)
  426. return () => 'child'
  427. }
  428. }
  429. const ChildB = {
  430. setup() {
  431. onBeforeUnmount(spyB)
  432. return () => 'child'
  433. }
  434. }
  435. const Parent = () => (
  436. openBlock(),
  437. createBlock('div', null, [
  438. (openBlock(true),
  439. createBlock(
  440. Fragment,
  441. null,
  442. [createVNode(ChildA, { key: 0 })],
  443. 128 /* KEYED_FRAGMENT */
  444. )),
  445. (openBlock(true),
  446. createBlock(
  447. Fragment,
  448. null,
  449. [createVNode(ChildB)],
  450. 256 /* UNKEYED_FRAGMENT */
  451. ))
  452. ])
  453. )
  454. render(h(Parent), root)
  455. render(null, root)
  456. expect(spyA).toHaveBeenCalledTimes(1)
  457. expect(spyB).toHaveBeenCalledTimes(1)
  458. })
  459. // #2893
  460. test('manually rendering the optimized slots should allow subsequent updates to exit the optimized mode correctly', async () => {
  461. const state = ref(0)
  462. const CompA = {
  463. setup(props: any, { slots }: SetupContext) {
  464. return () => {
  465. return (
  466. openBlock(),
  467. createBlock('div', null, [renderSlot(slots, 'default')])
  468. )
  469. }
  470. }
  471. }
  472. const Wrapper = {
  473. setup(props: any, { slots }: SetupContext) {
  474. // use the manually written render function to rendering the optimized slots,
  475. // which should make subsequent updates exit the optimized mode correctly
  476. return () => {
  477. return slots.default!()[state.value]
  478. }
  479. }
  480. }
  481. const app = createApp({
  482. setup() {
  483. return () => {
  484. return (
  485. openBlock(),
  486. createBlock(Wrapper, null, {
  487. default: withCtx(() => [
  488. createVNode(CompA, null, {
  489. default: withCtx(() => [createTextVNode('Hello')]),
  490. _: 1 /* STABLE */
  491. }),
  492. createVNode(CompA, null, {
  493. default: withCtx(() => [createTextVNode('World')]),
  494. _: 1 /* STABLE */
  495. })
  496. ]),
  497. _: 1 /* STABLE */
  498. })
  499. )
  500. }
  501. }
  502. })
  503. app.mount(root)
  504. expect(inner(root)).toBe('<div>Hello</div>')
  505. state.value = 1
  506. await nextTick()
  507. expect(inner(root)).toBe('<div>World</div>')
  508. })
  509. // #3548
  510. test('should not track dynamic children when the user calls a compiled slot inside template expression', () => {
  511. const Comp = {
  512. setup(props: any, { slots }: SetupContext) {
  513. return () => {
  514. return (
  515. openBlock(),
  516. (block = createBlock('section', null, [
  517. renderSlot(slots, 'default')
  518. ]))
  519. )
  520. }
  521. }
  522. }
  523. let dynamicVNode: VNode
  524. const Wrapper = {
  525. setup(props: any, { slots }: SetupContext) {
  526. return () => {
  527. return (
  528. openBlock(),
  529. createBlock(Comp, null, {
  530. default: withCtx(() => {
  531. return [
  532. (dynamicVNode = createVNode(
  533. 'div',
  534. {
  535. class: {
  536. foo: !!slots.default!()
  537. }
  538. },
  539. null,
  540. PatchFlags.CLASS
  541. ))
  542. ]
  543. }),
  544. _: 1
  545. })
  546. )
  547. }
  548. }
  549. }
  550. const app = createApp({
  551. render() {
  552. return (
  553. openBlock(),
  554. createBlock(Wrapper, null, {
  555. default: withCtx(() => {
  556. return [createVNode({}) /* component */]
  557. }),
  558. _: 1
  559. })
  560. )
  561. }
  562. })
  563. app.mount(root)
  564. expect(inner(root)).toBe('<section><div class="foo"></div></section>')
  565. /**
  566. * Block Tree:
  567. * - block(div)
  568. * - block(Fragment): renderSlots()
  569. * - dynamicVNode
  570. */
  571. expect(block!.dynamicChildren!.length).toBe(1)
  572. expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
  573. expect(block!.dynamicChildren![0].dynamicChildren![0]).toEqual(
  574. dynamicVNode!
  575. )
  576. })
  577. // 3569
  578. test('should force bailout when the user manually calls the slot function', async () => {
  579. const index = ref(0)
  580. const Foo = {
  581. setup(props: any, { slots }: SetupContext) {
  582. return () => {
  583. return slots.default!()[index.value]
  584. }
  585. }
  586. }
  587. const app = createApp({
  588. setup() {
  589. return () => {
  590. return (
  591. openBlock(),
  592. createBlock(Foo, null, {
  593. default: withCtx(() => [
  594. true
  595. ? (openBlock(), createBlock('p', { key: 0 }, '1'))
  596. : createCommentVNode('v-if', true),
  597. true
  598. ? (openBlock(), createBlock('p', { key: 0 }, '2'))
  599. : createCommentVNode('v-if', true)
  600. ]),
  601. _: 1 /* STABLE */
  602. })
  603. )
  604. }
  605. }
  606. })
  607. app.mount(root)
  608. expect(inner(root)).toBe('<p>1</p>')
  609. index.value = 1
  610. await nextTick()
  611. expect(inner(root)).toBe('<p>2</p>')
  612. index.value = 0
  613. await nextTick()
  614. expect(inner(root)).toBe('<p>1</p>')
  615. })
  616. })