feat: v-memo
This commit is contained in:
150
packages/runtime-core/__tests__/helpers/withMemo.spec.ts
Normal file
150
packages/runtime-core/__tests__/helpers/withMemo.spec.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
// since v-memo really is a compiler + runtime combo feature, we are performing
|
||||
// more of an itegration test here.
|
||||
import { ComponentOptions, createApp, nextTick } from 'vue'
|
||||
|
||||
describe('v-memo', () => {
|
||||
function mount(options: ComponentOptions): [HTMLElement, any] {
|
||||
const app = createApp(options)
|
||||
const el = document.createElement('div')
|
||||
const vm = app.mount(el)
|
||||
return [el, vm]
|
||||
}
|
||||
|
||||
test('on normal element', async () => {
|
||||
const [el, vm] = mount({
|
||||
template: `<div v-memo="[x]">{{ x }} {{ y }}</div>`,
|
||||
data: () => ({ x: 0, y: 0 })
|
||||
})
|
||||
expect(el.innerHTML).toBe(`<div>0 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.y++
|
||||
// should not update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>2 1</div>`)
|
||||
})
|
||||
|
||||
test('on component', async () => {
|
||||
const [el, vm] = mount({
|
||||
template: `<Comp v-memo="[x]" :x="x" :y="y"></Comp>`,
|
||||
data: () => ({ x: 0, y: 0 }),
|
||||
components: {
|
||||
Comp: {
|
||||
props: ['x', 'y'],
|
||||
template: `<div>{{x}} {{y}}</div>`
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(el.innerHTML).toBe(`<div>0 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.y++
|
||||
// should not update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>2 1</div>`)
|
||||
})
|
||||
|
||||
test('on v-if', async () => {
|
||||
const [el, vm] = mount({
|
||||
template: `<div v-if="ok" v-memo="[x]">{{ x }} {{ y }}</div>
|
||||
<div v-else v-memo="[y]">{{ y }} {{ x }}</div>`,
|
||||
data: () => ({ ok: true, x: 0, y: 0 })
|
||||
})
|
||||
expect(el.innerHTML).toBe(`<div>0 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.y++
|
||||
// should not update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 0</div>`)
|
||||
|
||||
vm.x++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>2 1</div>`)
|
||||
|
||||
vm.ok = false
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>1 2</div>`)
|
||||
|
||||
vm.y++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>2 2</div>`)
|
||||
|
||||
vm.x++
|
||||
// should not update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>2 2</div>`)
|
||||
|
||||
vm.y++
|
||||
// should update
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(`<div>3 3</div>`)
|
||||
})
|
||||
|
||||
test('on v-for', async () => {
|
||||
const [el, vm] = mount({
|
||||
template:
|
||||
`<div v-for="{ x } in list" :key="x" v-memo="[x, x === y]">` +
|
||||
`{{ x }} {{ x === y ? 'yes' : 'no' }} {{ z }}` +
|
||||
`</div>`,
|
||||
data: () => ({
|
||||
list: [{ x: 1 }, { x: 2 }, { x: 3 }],
|
||||
y: 1,
|
||||
z: 'z'
|
||||
})
|
||||
})
|
||||
expect(el.innerHTML).toBe(
|
||||
`<div>1 yes z</div><div>2 no z</div><div>3 no z</div>`
|
||||
)
|
||||
|
||||
vm.y = 2
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(
|
||||
`<div>1 no z</div><div>2 yes z</div><div>3 no z</div>`
|
||||
)
|
||||
|
||||
vm.list[0].x = 4
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(
|
||||
`<div>4 no z</div><div>2 yes z</div><div>3 no z</div>`
|
||||
)
|
||||
|
||||
vm.list[0].x = 5
|
||||
vm.y = 5
|
||||
await nextTick()
|
||||
expect(el.innerHTML).toBe(
|
||||
`<div>5 yes z</div><div>2 no z</div><div>3 no z</div>`
|
||||
)
|
||||
|
||||
vm.z = 'zz'
|
||||
await nextTick()
|
||||
// should not update
|
||||
expect(el.innerHTML).toBe(
|
||||
`<div>5 yes z</div><div>2 no z</div><div>3 no z</div>`
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VNodeChild } from '../vnode'
|
||||
import { VNode, VNodeChild } from '../vnode'
|
||||
import { isArray, isString, isObject } from '@vue/shared'
|
||||
import { warn } from '../warning'
|
||||
|
||||
@@ -52,13 +52,17 @@ export function renderList<T>(
|
||||
*/
|
||||
export function renderList(
|
||||
source: any,
|
||||
renderItem: (...args: any[]) => VNodeChild
|
||||
renderItem: (...args: any[]) => VNodeChild,
|
||||
cache?: any[],
|
||||
index?: number
|
||||
): VNodeChild[] {
|
||||
let ret: VNodeChild[]
|
||||
const cached = (cache && cache[index!]) as VNode[] | undefined
|
||||
|
||||
if (isArray(source) || isString(source)) {
|
||||
ret = new Array(source.length)
|
||||
for (let i = 0, l = source.length; i < l; i++) {
|
||||
ret[i] = renderItem(source[i], i)
|
||||
ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
|
||||
}
|
||||
} else if (typeof source === 'number') {
|
||||
if (__DEV__ && !Number.isInteger(source)) {
|
||||
@@ -71,17 +75,23 @@ export function renderList(
|
||||
}
|
||||
} else if (isObject(source)) {
|
||||
if (source[Symbol.iterator as any]) {
|
||||
ret = Array.from(source as Iterable<any>, renderItem)
|
||||
ret = Array.from(source as Iterable<any>, (item, i) =>
|
||||
renderItem(item, i, undefined, cached && cached[i])
|
||||
)
|
||||
} else {
|
||||
const keys = Object.keys(source)
|
||||
ret = new Array(keys.length)
|
||||
for (let i = 0, l = keys.length; i < l; i++) {
|
||||
const key = keys[i]
|
||||
ret[i] = renderItem(source[key], key, i)
|
||||
ret[i] = renderItem(source[key], key, i, cached && cached[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ret = []
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
cache[index!] = ret
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
29
packages/runtime-core/src/helpers/withMemo.ts
Normal file
29
packages/runtime-core/src/helpers/withMemo.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { currentBlock, isBlockTreeEnabled, VNode } from '../vnode'
|
||||
|
||||
export function withMemo(
|
||||
memo: any[],
|
||||
render: () => VNode,
|
||||
cache: any[],
|
||||
index: number
|
||||
) {
|
||||
const cached = cache[index] as VNode | undefined
|
||||
if (cached && isMemoSame(cached.memo!, memo)) {
|
||||
// make sure to let parent block track it when returning cached
|
||||
if (isBlockTreeEnabled > 0 && currentBlock) {
|
||||
currentBlock.push(cached)
|
||||
}
|
||||
return cached
|
||||
}
|
||||
const ret = render()
|
||||
ret.memo = memo
|
||||
return (cache[index] = ret)
|
||||
}
|
||||
|
||||
export function isMemoSame(prev: any[], next: any[]) {
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (prev[i] !== next[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -264,6 +264,7 @@ export { renderList } from './helpers/renderList'
|
||||
export { toHandlers } from './helpers/toHandlers'
|
||||
export { renderSlot } from './helpers/renderSlot'
|
||||
export { createSlots } from './helpers/createSlots'
|
||||
export { withMemo, isMemoSame } from './helpers/withMemo'
|
||||
export {
|
||||
openBlock,
|
||||
createBlock,
|
||||
|
||||
@@ -182,6 +182,9 @@ export interface VNode<
|
||||
|
||||
// application root node only
|
||||
appContext: AppContext | null
|
||||
|
||||
// v-for memo
|
||||
memo?: any[]
|
||||
}
|
||||
|
||||
// Since v-if and v-for are the two possible ways node structure can dynamically
|
||||
@@ -221,7 +224,7 @@ export function closeBlock() {
|
||||
// Only tracks when this value is > 0
|
||||
// We are not using a simple boolean because this value may need to be
|
||||
// incremented/decremented by nested usage of v-once (see below)
|
||||
let isBlockTreeEnabled = 1
|
||||
export let isBlockTreeEnabled = 1
|
||||
|
||||
/**
|
||||
* Block tracking sometimes needs to be disabled, for example during the
|
||||
@@ -692,7 +695,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
|
||||
|
||||
// optimized normalization for template-compiled render fns
|
||||
export function cloneIfMounted(child: VNode): VNode {
|
||||
return child.el === null ? child : cloneVNode(child)
|
||||
return child.el === null || child.memo ? child : cloneVNode(child)
|
||||
}
|
||||
|
||||
export function normalizeChildren(vnode: VNode, children: unknown) {
|
||||
|
||||
Reference in New Issue
Block a user