vue3-yuanma/packages/runtime-core/__tests__/vnode.spec.ts
Evan You d901b6bea8 refactor(reactivity): use more efficient reactive checks
WeakSets and WeakMaps shows degrading performance as the amount of
observed objects increases. Using hidden keys result in better
performance especially when repeatedly creating large amounts of
reactive proxies.

This also makes it possible to more efficiently declare non-reactive
objects in userland.
2020-05-02 16:58:17 -04:00

437 lines
13 KiB
TypeScript

import {
createBlock,
createVNode,
openBlock,
Comment,
Fragment,
Text,
cloneVNode,
mergeProps,
normalizeVNode,
transformVNodeArgs
} from '../src/vnode'
import { Data } from '../src/component'
import { ShapeFlags, PatchFlags } from '@vue/shared'
import { h, reactive, isReactive } from '../src'
import { createApp, nodeOps, serializeInner } from '@vue/runtime-test'
describe('vnode', () => {
test('create with just tag', () => {
const vnode = createVNode('p')
expect(vnode.type).toBe('p')
expect(vnode.props).toBe(null)
})
test('create with tag and props', () => {
const vnode = createVNode('p', {})
expect(vnode.type).toBe('p')
expect(vnode.props).toMatchObject({})
})
test('create with tag, props and children', () => {
const vnode = createVNode('p', {}, ['foo'])
expect(vnode.type).toBe('p')
expect(vnode.props).toMatchObject({})
expect(vnode.children).toMatchObject(['foo'])
})
test('create with 0 as props', () => {
const vnode = createVNode('p', null)
expect(vnode.type).toBe('p')
expect(vnode.props).toBe(null)
})
test('vnode keys', () => {
for (const key of ['', 'a', 0, 1, NaN]) {
expect(createVNode('div', { key }).key).toBe(key)
}
expect(createVNode('div').key).toBe(null)
expect(createVNode('div', { key: undefined }).key).toBe(null)
})
test('create with class component', () => {
class Component {
$props: any
static __vccOpts = { template: '<div />' }
}
const vnode = createVNode(Component)
expect(vnode.type).toEqual(Component.__vccOpts)
})
describe('class normalization', () => {
test('string', () => {
const vnode = createVNode('p', { class: 'foo baz' })
expect(vnode.props).toMatchObject({ class: 'foo baz' })
})
test('array<string>', () => {
const vnode = createVNode('p', { class: ['foo', 'baz'] })
expect(vnode.props).toMatchObject({ class: 'foo baz' })
})
test('array<object>', () => {
const vnode = createVNode('p', {
class: [{ foo: 'foo' }, { baz: 'baz' }]
})
expect(vnode.props).toMatchObject({ class: 'foo baz' })
})
test('object', () => {
const vnode = createVNode('p', { class: { foo: 'foo', baz: 'baz' } })
expect(vnode.props).toMatchObject({ class: 'foo baz' })
})
})
describe('style normalization', () => {
test('array', () => {
const vnode = createVNode('p', {
style: [{ foo: 'foo' }, { baz: 'baz' }]
})
expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
})
test('object', () => {
const vnode = createVNode('p', { style: { foo: 'foo', baz: 'baz' } })
expect(vnode.props).toMatchObject({ style: { foo: 'foo', baz: 'baz' } })
})
})
describe('children normalization', () => {
const nop = jest.fn
test('null', () => {
const vnode = createVNode('p', null, null)
expect(vnode.children).toBe(null)
expect(vnode.shapeFlag).toBe(ShapeFlags.ELEMENT)
})
test('array', () => {
const vnode = createVNode('p', null, ['foo'])
expect(vnode.children).toMatchObject(['foo'])
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
)
})
test('object', () => {
const vnode = createVNode('p', null, { foo: 'foo' })
expect(vnode.children).toMatchObject({ foo: 'foo' })
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
)
})
test('function', () => {
const vnode = createVNode('p', null, nop)
expect(vnode.children).toMatchObject({ default: nop })
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.SLOTS_CHILDREN
)
})
test('string', () => {
const vnode = createVNode('p', null, 'foo')
expect(vnode.children).toBe('foo')
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
)
})
test('element with slots', () => {
const children = [createVNode('span', null, 'hello')]
const vnode = createVNode('div', null, {
default: () => children
})
expect(vnode.children).toBe(children)
expect(vnode.shapeFlag).toBe(
ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN
)
})
})
test('normalizeVNode', () => {
// null / undefined -> Comment
expect(normalizeVNode(null)).toMatchObject({ type: Comment })
expect(normalizeVNode(undefined)).toMatchObject({ type: Comment })
// boolean -> Comment
// this is for usage like `someBoolean && h('div')` and behavior consistency
// with 2.x (#574)
expect(normalizeVNode(true)).toMatchObject({ type: Comment })
expect(normalizeVNode(false)).toMatchObject({ type: Comment })
// array -> Fragment
expect(normalizeVNode(['foo'])).toMatchObject({ type: Fragment })
// VNode -> VNode
const vnode = createVNode('div')
expect(normalizeVNode(vnode)).toBe(vnode)
// mounted VNode -> cloned VNode
const mounted = createVNode('div')
mounted.el = {}
const normalized = normalizeVNode(mounted)
expect(normalized).not.toBe(mounted)
expect(normalized).toEqual(mounted)
// primitive types
expect(normalizeVNode('foo')).toMatchObject({ type: Text, children: `foo` })
expect(normalizeVNode(1)).toMatchObject({ type: Text, children: `1` })
})
test('type shapeFlag inference', () => {
expect(createVNode('div').shapeFlag).toBe(ShapeFlags.ELEMENT)
expect(createVNode({}).shapeFlag).toBe(ShapeFlags.STATEFUL_COMPONENT)
expect(createVNode(() => {}).shapeFlag).toBe(
ShapeFlags.FUNCTIONAL_COMPONENT
)
expect(createVNode(Text).shapeFlag).toBe(0)
})
test('cloneVNode', () => {
const node1 = createVNode('div', { foo: 1 }, null)
expect(cloneVNode(node1)).toEqual(node1)
const node2 = createVNode({}, null, [node1])
const cloned2 = cloneVNode(node2)
expect(cloned2).toEqual(node2)
expect(cloneVNode(node2)).toEqual(node2)
expect(cloneVNode(node2)).toEqual(cloned2)
// #1041 should use resolved key/ref
expect(cloneVNode(createVNode('div', { key: 1 })).key).toBe(1)
expect(cloneVNode(createVNode('div', { key: 1 }), { key: 2 }).key).toBe(2)
expect(cloneVNode(createVNode('div'), { key: 2 }).key).toBe(2)
// ref normalizes to [currentRenderingInstance, ref]
expect(cloneVNode(createVNode('div', { ref: 'foo' })).ref).toEqual([
null,
'foo'
])
expect(
cloneVNode(createVNode('div', { ref: 'foo' }), { ref: 'bar' }).ref
).toEqual([null, 'bar'])
expect(cloneVNode(createVNode('div'), { ref: 'bar' }).ref).toEqual([
null,
'bar'
])
})
describe('mergeProps', () => {
test('class', () => {
let props1: Data = { class: 'c' }
let props2: Data = { class: ['cc'] }
let props3: Data = { class: [{ ccc: true }] }
let props4: Data = { class: { cccc: true } }
expect(mergeProps(props1, props2, props3, props4)).toMatchObject({
class: 'c cc ccc cccc'
})
})
test('style', () => {
let props1: Data = {
style: {
color: 'red',
fontSize: 10
}
}
let props2: Data = {
style: [
{
color: 'blue',
width: '200px'
},
{
width: '300px',
height: '300px',
fontSize: 30
}
]
}
expect(mergeProps(props1, props2)).toMatchObject({
style: {
color: 'blue',
width: '300px',
height: '300px',
fontSize: 30
}
})
})
test('handlers', () => {
let clickHandler1 = function() {}
let clickHandler2 = function() {}
let focusHandler2 = function() {}
let props1: Data = { onClick: clickHandler1 }
let props2: Data = { onClick: clickHandler2, onFocus: focusHandler2 }
expect(mergeProps(props1, props2)).toMatchObject({
onClick: [clickHandler1, clickHandler2],
onFocus: focusHandler2
})
})
test('default', () => {
let props1: Data = { foo: 'c' }
let props2: Data = { foo: {}, bar: ['cc'] }
let props3: Data = { baz: { ccc: true } }
expect(mergeProps(props1, props2, props3)).toMatchObject({
foo: {},
bar: ['cc'],
baz: { ccc: true }
})
})
})
describe('dynamic children', () => {
test('with patchFlags', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('should not track vnodes with only HYDRATE_EVENTS flag', () => {
const hoist = createVNode('div')
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
createVNode('div', null, 'text', PatchFlags.HYDRATE_EVENTS)
]))
expect(vnode.dynamicChildren).toStrictEqual([])
})
test('many times call openBlock', () => {
const hoist = createVNode('div')
let vnode1, vnode2, vnode3
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT)),
(vnode2 = (openBlock(),
createBlock('div', null, [
hoist,
(vnode3 = createVNode('div', null, 'text', PatchFlags.TEXT))
])))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1, vnode2])
expect(vnode2.dynamicChildren).toStrictEqual([vnode3])
})
test('with stateful component', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode({}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('with functional component', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
test('with suspense', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, [
hoist,
(vnode1 = createVNode(() => {}, null, 'text'))
]))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
// #1039
// <component :is="foo">{{ bar }}</component>
// - content is compiled as slot
// - dynamic component resolves to plain element, but as a block
// - block creation disables its own tracking, accidentally causing the
// slot content (called during the block node creation) to be missed
test('element block should track normalized slot children', () => {
const hoist = createVNode('div')
let vnode1
const vnode = (openBlock(),
createBlock('div', null, {
default: () => {
return [
hoist,
(vnode1 = createVNode('div', null, 'text', PatchFlags.TEXT))
]
}
}))
expect(vnode.dynamicChildren).toStrictEqual([vnode1])
})
})
describe('transformVNodeArgs', () => {
afterEach(() => {
// reset
transformVNodeArgs()
})
test('no-op pass through', () => {
transformVNodeArgs(args => args)
const vnode = createVNode('div', { id: 'foo' }, 'hello')
expect(vnode).toMatchObject({
type: 'div',
props: { id: 'foo' },
children: 'hello',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
})
})
test('direct override', () => {
transformVNodeArgs(() => ['div', { id: 'foo' }, 'hello'])
const vnode = createVNode('p')
expect(vnode).toMatchObject({
type: 'div',
props: { id: 'foo' },
children: 'hello',
shapeFlag: ShapeFlags.ELEMENT | ShapeFlags.TEXT_CHILDREN
})
})
test('receive component instance as 2nd arg', () => {
transformVNodeArgs((args, instance) => {
if (instance) {
return ['h1', null, instance.type.name]
} else {
return args
}
})
const App = {
// this will be the name of the component in the h1
name: 'Root Component',
render() {
return h('p') // this will be overwritten by the transform
}
}
const root = nodeOps.createElement('div')
createApp(App).mount(root)
expect(serializeInner(root)).toBe('<h1>Root Component</h1>')
})
test('should not be observable', () => {
const a = createVNode('div')
const b = reactive(a)
expect(b).toBe(a)
expect(isReactive(b)).toBe(false)
})
})
})