| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- import {
- h,
- Fragment,
- createVNode,
- openBlock,
- createBlock,
- render,
- nodeOps,
- TestElement,
- serialize,
- serializeInner as inner,
- VNode,
- ref,
- nextTick,
- defineComponent,
- withCtx,
- renderSlot,
- onBeforeUnmount
- } from '@vue/runtime-test'
- import { PatchFlags, SlotFlags } from '@vue/shared'
- describe('renderer: optimized mode', () => {
- let root: TestElement
- let block: VNode | null = null
- beforeEach(() => {
- root = nodeOps.createElement('div')
- block = null
- })
- const renderWithBlock = (renderChildren: () => VNode[]) => {
- render(
- (openBlock(), (block = createBlock('div', null, renderChildren()))),
- root
- )
- }
- test('basic use of block', () => {
- render((openBlock(), (block = createBlock('p', null, 'foo'))), root)
- expect(block.dynamicChildren!.length).toBe(0)
- expect(inner(root)).toBe('<p>foo</p>')
- })
- test('block can appear anywhere in the vdom tree', () => {
- render(
- h('div', (openBlock(), (block = createBlock('p', null, 'foo')))),
- root
- )
- expect(block.dynamicChildren!.length).toBe(0)
- expect(inner(root)).toBe('<div><p>foo</p></div>')
- })
- test('block should collect dynamic vnodes', () => {
- renderWithBlock(() => [
- createVNode('p', null, 'foo', PatchFlags.TEXT),
- createVNode('i')
- ])
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foo</p>'
- )
- })
- test('block can disable tracking', () => {
- render(
- // disable tracking
- (openBlock(true),
- (block = createBlock('div', null, [
- createVNode('p', null, 'foo', PatchFlags.TEXT)
- ]))),
- root
- )
- expect(block.dynamicChildren!.length).toBe(0)
- })
- test('block as dynamic children', () => {
- renderWithBlock(() => [
- (openBlock(), createBlock('div', { key: 0 }, [h('p')]))
- ])
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<div><p></p></div>'
- )
- renderWithBlock(() => [
- (openBlock(), createBlock('div', { key: 1 }, [h('i')]))
- ])
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(0)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<div><i></i></div>'
- )
- })
- test('PatchFlags: PatchFlags.TEXT', async () => {
- renderWithBlock(() => [createVNode('p', null, 'foo', PatchFlags.TEXT)])
- expect(inner(root)).toBe('<div><p>foo</p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foo</p>'
- )
- renderWithBlock(() => [createVNode('p', null, 'bar', PatchFlags.TEXT)])
- expect(inner(root)).toBe('<div><p>bar</p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p>bar</p>'
- )
- })
- test('PatchFlags: PatchFlags.CLASS', async () => {
- renderWithBlock(() => [
- createVNode('p', { class: 'foo' }, '', PatchFlags.CLASS)
- ])
- expect(inner(root)).toBe('<div><p class="foo"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p class="foo"></p>'
- )
- renderWithBlock(() => [
- createVNode('p', { class: 'bar' }, '', PatchFlags.CLASS)
- ])
- expect(inner(root)).toBe('<div><p class="bar"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p class="bar"></p>'
- )
- })
- test('PatchFlags: PatchFlags.STYLE', async () => {
- renderWithBlock(() => [
- createVNode('p', { style: 'color: red' }, '', PatchFlags.STYLE)
- ])
- expect(inner(root)).toBe('<div><p style="color: red"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p style="color: red"></p>'
- )
- renderWithBlock(() => [
- createVNode('p', { style: 'color: green' }, '', PatchFlags.STYLE)
- ])
- expect(inner(root)).toBe('<div><p style="color: green"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p style="color: green"></p>'
- )
- })
- test('PatchFlags: PatchFlags.PROPS', async () => {
- renderWithBlock(() => [
- createVNode('p', { id: 'foo' }, '', PatchFlags.PROPS, ['id'])
- ])
- expect(inner(root)).toBe('<div><p id="foo"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p id="foo"></p>'
- )
- renderWithBlock(() => [
- createVNode('p', { id: 'bar' }, '', PatchFlags.PROPS, ['id'])
- ])
- expect(inner(root)).toBe('<div><p id="bar"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p id="bar"></p>'
- )
- })
- test('PatchFlags: PatchFlags.FULL_PROPS', async () => {
- let propName = 'foo'
- renderWithBlock(() => [
- createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
- ])
- expect(inner(root)).toBe('<div><p foo="dynamic"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p foo="dynamic"></p>'
- )
- propName = 'bar'
- renderWithBlock(() => [
- createVNode('p', { [propName]: 'dynamic' }, '', PatchFlags.FULL_PROPS)
- ])
- expect(inner(root)).toBe('<div><p bar="dynamic"></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p bar="dynamic"></p>'
- )
- })
- // the order and length of the list will not change
- test('PatchFlags: PatchFlags.STABLE_FRAGMENT', async () => {
- let list = ['foo', 'bar']
- render(
- (openBlock(),
- (block = createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode('p', null, item, PatchFlags.TEXT)
- }),
- PatchFlags.STABLE_FRAGMENT
- ))),
- root
- )
- expect(inner(root)).toBe('<p>foo</p><p>bar</p>')
- expect(block.dynamicChildren!.length).toBe(2)
- expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foo</p>'
- )
- expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
- '<p>bar</p>'
- )
- list = list.map(item => item.repeat(2))
- render(
- (openBlock(),
- createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode('p', null, item, PatchFlags.TEXT)
- }),
- PatchFlags.STABLE_FRAGMENT
- )),
- root
- )
- expect(inner(root)).toBe('<p>foofoo</p><p>barbar</p>')
- expect(block.dynamicChildren!.length).toBe(2)
- expect(serialize(block.dynamicChildren![0].el as TestElement)).toBe(
- '<p>foofoo</p>'
- )
- expect(serialize(block.dynamicChildren![1].el as TestElement)).toBe(
- '<p>barbar</p>'
- )
- })
- // A Fragment with `UNKEYED_FRAGMENT` flag will always patch its children,
- // so there's no need for tracking dynamicChildren.
- test('PatchFlags: PatchFlags.UNKEYED_FRAGMENT', async () => {
- const list = [{ tag: 'p', text: 'foo' }]
- render(
- (openBlock(true),
- (block = createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode(item.tag, null, item.text)
- }),
- PatchFlags.UNKEYED_FRAGMENT
- ))),
- root
- )
- expect(inner(root)).toBe('<p>foo</p>')
- expect(block.dynamicChildren!.length).toBe(0)
- list.unshift({ tag: 'i', text: 'bar' })
- render(
- (openBlock(true),
- createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode(item.tag, null, item.text)
- }),
- PatchFlags.UNKEYED_FRAGMENT
- )),
- root
- )
- expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
- expect(block.dynamicChildren!.length).toBe(0)
- })
- // A Fragment with `KEYED_FRAGMENT` will always patch its children,
- // so there's no need for tracking dynamicChildren.
- test('PatchFlags: PatchFlags.KEYED_FRAGMENT', async () => {
- const list = [{ tag: 'p', text: 'foo' }]
- render(
- (openBlock(true),
- (block = createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode(item.tag, { key: item.tag }, item.text)
- }),
- PatchFlags.KEYED_FRAGMENT
- ))),
- root
- )
- expect(inner(root)).toBe('<p>foo</p>')
- expect(block.dynamicChildren!.length).toBe(0)
- list.unshift({ tag: 'i', text: 'bar' })
- render(
- (openBlock(true),
- createBlock(
- Fragment,
- null,
- list.map(item => {
- return createVNode(item.tag, { key: item.tag }, item.text)
- }),
- PatchFlags.KEYED_FRAGMENT
- )),
- root
- )
- expect(inner(root)).toBe('<i>bar</i><p>foo</p>')
- expect(block.dynamicChildren!.length).toBe(0)
- })
- test('PatchFlags: PatchFlags.NEED_PATCH', async () => {
- const spyMounted = jest.fn()
- const spyUpdated = jest.fn()
- const count = ref(0)
- const Comp = {
- setup() {
- return () => {
- count.value
- return (
- openBlock(),
- (block = createBlock('div', null, [
- createVNode(
- 'p',
- { onVnodeMounted: spyMounted, onVnodeBeforeUpdate: spyUpdated },
- '',
- PatchFlags.NEED_PATCH
- )
- ]))
- )
- }
- }
- }
- render(h(Comp), root)
- expect(inner(root)).toBe('<div><p></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p></p>'
- )
- expect(spyMounted).toHaveBeenCalledTimes(1)
- expect(spyUpdated).toHaveBeenCalledTimes(0)
- count.value++
- await nextTick()
- expect(inner(root)).toBe('<div><p></p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(serialize(block!.dynamicChildren![0].el as TestElement)).toBe(
- '<p></p>'
- )
- expect(spyMounted).toHaveBeenCalledTimes(1)
- expect(spyUpdated).toHaveBeenCalledTimes(1)
- })
- test('PatchFlags: PatchFlags.BAIL', async () => {
- render(
- (openBlock(),
- (block = createBlock('div', null, [createVNode('p', null, 'foo')]))),
- root
- )
- expect(inner(root)).toBe('<div><p>foo</p></div>')
- expect(block!.dynamicChildren!.length).toBe(0)
- render(
- (openBlock(),
- (block = createBlock(
- 'div',
- null,
- [createVNode('i', null, 'bar')],
- PatchFlags.BAIL
- ))),
- root
- )
- expect(inner(root)).toBe('<div><i>bar</i></div>')
- expect(block!.dynamicChildren).toBe(null)
- })
- // #1980
- test('dynamicChildren should be tracked correctly when normalizing slots to plain children', async () => {
- let block: VNode
- const Comp = defineComponent({
- setup(_props, { slots }) {
- return () => {
- const vnode = (openBlock(),
- (block = createBlock('div', null, {
- default: withCtx(() => [renderSlot(slots, 'default')]),
- _: SlotFlags.FORWARDED
- })))
- return vnode
- }
- }
- })
- const foo = ref(0)
- const App = {
- setup() {
- return () => {
- return createVNode(Comp, null, {
- default: withCtx(() => [
- createVNode('p', null, foo.value, PatchFlags.TEXT)
- ]),
- // Indicates that this is a stable slot to avoid bail out
- _: SlotFlags.STABLE
- })
- }
- }
- }
- render(h(App), root)
- expect(inner(root)).toBe('<div><p>0</p></div>')
- expect(block!.dynamicChildren!.length).toBe(1)
- expect(block!.dynamicChildren![0].type).toBe(Fragment)
- expect(block!.dynamicChildren![0].dynamicChildren!.length).toBe(1)
- expect(
- serialize(block!.dynamicChildren![0].dynamicChildren![0]
- .el as TestElement)
- ).toBe('<p>0</p>')
- foo.value++
- await nextTick()
- expect(inner(root)).toBe('<div><p>1</p></div>')
- })
- // #2169
- // block
- // - dynamic child (1)
- // - component (2)
- // When unmounting (1), we know we are in optimized mode so no need to further
- // traverse unmount its children
- test('should not perform unnecessary unmount traversals', () => {
- const spy = jest.fn()
- const Child = {
- setup() {
- onBeforeUnmount(spy)
- return () => 'child'
- }
- }
- const Parent = () => (
- openBlock(),
- createBlock('div', null, [
- createVNode('div', { style: {} }, [createVNode(Child)], 4 /* STYLE */)
- ])
- )
- render(h(Parent), root)
- render(null, root)
- expect(spy).toHaveBeenCalledTimes(1)
- })
- // #2444
- // `KEYED_FRAGMENT` and `UNKEYED_FRAGMENT` always need to diff its children
- test('non-stable Fragment always need to diff its children', () => {
- const spyA = jest.fn()
- const spyB = jest.fn()
- const ChildA = {
- setup() {
- onBeforeUnmount(spyA)
- return () => 'child'
- }
- }
- const ChildB = {
- setup() {
- onBeforeUnmount(spyB)
- return () => 'child'
- }
- }
- const Parent = () => (
- openBlock(),
- createBlock('div', null, [
- (openBlock(true),
- createBlock(
- Fragment,
- null,
- [createVNode(ChildA, { key: 0 })],
- 128 /* KEYED_FRAGMENT */
- )),
- (openBlock(true),
- createBlock(
- Fragment,
- null,
- [createVNode(ChildB)],
- 256 /* UNKEYED_FRAGMENT */
- ))
- ])
- )
- render(h(Parent), root)
- render(null, root)
- expect(spyA).toHaveBeenCalledTimes(1)
- expect(spyB).toHaveBeenCalledTimes(1)
- })
- })
|