rendererOptimizedMode.spec.ts 14 KB

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