rendererOptimizedMode.spec.ts 15 KB

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