Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vapor-runtime): object support in v-for #139

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/runtime-vapor/__tests__/for.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,46 @@ describe('createFor', () => {
await nextTick()
expect(host.innerHTML).toBe('<!--for-->')
})

test('basic object', async () => {
const obj = ref<Record<string, any>>({ a: 1, b: 2, c: 3 })

const { host } = define(() => {
const n1 = createFor(
() => obj.value,
block => {
const n3 = createTextNode()
const update = () => {
const [item, key] = block.s
setText(n3, item + key)
}
renderEffect(update)
return [n3, update]
},
)
return [n1]
}).render()

expect(host.innerHTML).toBe('1a2b3c<!--for-->')

// add
obj.value = { ...obj.value, d: 4 }
await nextTick()
expect(host.innerHTML).toBe('1a2b3c4d<!--for-->')

// move
obj.value = { d: 4, c: 3, b: 2, a: 1 }
await nextTick()
expect(host.innerHTML).toBe('4d3c2b1a<!--for-->')

// change
obj.value = { e: 'E', f: 'F', g: 'G' }
await nextTick()
expect(host.innerHTML).toBe('EeFfGg<!--for-->')

// remove
obj.value = {}
await nextTick()
expect(host.innerHTML).toBe('<!--for-->')
})
})
100 changes: 82 additions & 18 deletions packages/runtime-vapor/src/for.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { type EffectScope, effectScope, isReactive } from '@vue/reactivity'
import { isArray } from '@vue/shared'
import { isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode, insert, remove } from './dom/element'
import { renderEffect } from './renderWatch'
import { type Block, type Fragment, fragmentKey } from './render'

interface ForBlock extends Fragment {
scope: EffectScope
s: [any, number] // state, use short key since it's used a lot in generated code
s: [any, number | string] // state, use short key since it's used a lot in generated code
update: () => void
key: any
memo: any[] | undefined
}

/*! #__NO_SIDE_EFFECTS__ */
export const createFor = (
src: () => any[] | Record<string, string> | Set<any> | Map<any, any>,
src: () => any[] | Record<string, any> | Set<any> | Map<any, any>,
renderItem: (block: ForBlock) => [Block, () => void],
getKey?: (item: any, index: number) => any,
getMemo?: (item: any) => any[],
Expand All @@ -34,14 +34,14 @@ export const createFor = (
item: any,
index: number,
anchor: Node = parentAnchor,
key?: string,
): ForBlock => {
const scope = effectScope()
// TODO support object keys etc.
const block: ForBlock = (newBlocks[index] = {
nodes: null as any,
update: null as any,
scope,
s: [item, index],
s: [item, key || index],
key: getKey && getKey(item, index),
memo: getMemo && getMemo(item),
[fragmentKey]: true,
Expand All @@ -54,10 +54,20 @@ export const createFor = (
return block
}

const mountList = (source: any[], offset = 0) => {
const mountList = (source: any[], sourceKeys: null | any[], offset = 0) => {
if (offset) source = source.slice(offset)
for (let i = 0, l = source.length; i < l; i++) {
mount(source[i], i + offset)

if (sourceKeys) {
if (offset) sourceKeys = sourceKeys.slice(offset)

for (let i = 0, l = source.length; i < l; i++) {
const key = sourceKeys[i]
mount(source[i], i + offset, parentAnchor, key)
}
} else {
for (let i = 0, l = source.length; i < l; i++) {
mount(source[i], i + offset)
}
}
}

Expand Down Expand Up @@ -114,20 +124,21 @@ export const createFor = (
}

renderEffect(() => {
// TODO support more than arrays
const source = src() as any[]
const { resolvedSource: source, resolvedSourceKeys: sourceKeys } =
sourceProcessHelper(src())

const newLength = source.length
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)

newBlocks = new Array(newLength)
if (!isMounted) {
isMounted = true
mountList(source)
mountList(source, sourceKeys)
} else {
parent = parent || parentAnchor.parentNode
if (!oldLength) {
// fast path for all new
mountList(source)
mountList(source, sourceKeys)
} else if (!newLength) {
// fast path for clearing
for (let i = 0; i < oldLength; i++) {
Expand All @@ -136,10 +147,22 @@ export const createFor = (
} else if (!getKey) {
// unkeyed fast path
const commonLength = Math.min(newLength, oldLength)
for (let i = 0; i < commonLength; i++) {
update((newBlocks[i] = oldBlocks[i]), source[i])
if (sourceKeys) {
for (let i = 0; i < commonLength; i++) {
const key = sourceKeys[i]
update(
(newBlocks[i] = oldBlocks[i]),
source[i],
newBlocks[i].s[1],
key,
)
}
} else {
for (let i = 0; i < commonLength; i++) {
update((newBlocks[i] = oldBlocks[i]), source[i])
}
}
mountList(source, oldLength)
mountList(source, sourceKeys, oldLength)
for (let i = newLength; i < oldLength; i++) {
unmount(oldBlocks[i])
}
Expand Down Expand Up @@ -186,7 +209,12 @@ export const createFor = (
? normalizeAnchor(newBlocks[nextPos].nodes)
: parentAnchor
while (i <= e2) {
mount(source[i], i, anchor)
if (sourceKeys) {
const key = sourceKeys[i]
mount(source[i], i, anchor, key)
} else {
mount(source[i], i, anchor)
}
i++
}
}
Expand Down Expand Up @@ -251,12 +279,14 @@ export const createFor = (
} else {
moved = true
}

update(
(newBlocks[newIndex] = prevBlock),
source[newIndex],
i,
newIndex,
)

patched++
}
}
Expand All @@ -277,7 +307,12 @@ export const createFor = (
: parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
mount(source[nextIndex], nextIndex, anchor)
if (sourceKeys) {
const key = sourceKeys[nextIndex]
mount(source[nextIndex], nextIndex, anchor, key)
} else {
mount(source[nextIndex], nextIndex, anchor)
}
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
Expand Down Expand Up @@ -350,3 +385,32 @@ const getSequence = (arr: number[]): number[] => {
}
return result
}

/**
* translate all the source type to an Array
*/
const sourceProcessHelper = (source: any) => {
let resolvedSource: any[] = []
let resolvedSourceKeys: any[] | null = null

if (isString(source)) {
resolvedSource = source.split('') as string[]
} else if (typeof source === 'number') {
resolvedSource = Array.from({ length: source }, (_, index) => index + 1)
} else if (isArray(source)) {
resolvedSource = source
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
resolvedSource = Array.from(source as Iterable<any>, (item, _) => item)
} else {
resolvedSourceKeys = Object.keys(source)
for (let i = 0; i < resolvedSourceKeys.length; i++) {
const key = resolvedSourceKeys[i]
resolvedSource[i] = source[key]
}
}
} else {
resolvedSource = []
}
return { resolvedSource, resolvedSourceKeys }
}
51 changes: 51 additions & 0 deletions playground/src/v-for.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref } from 'vue/vapor'

const arr = ref([1, 2, 3])
const obj = ref<Record<string, any>>({ a: 1, b: 2, c: 3 })
const str = ref('123')
const map = ref(
new Map([
['a', 1],
['b', 2],
['c', 3],
]),
)
const num = ref(10)

const objChange = () => {
obj.value = { a: 'x', e: 4, f: 5, g: 6 }
}

const arrChange = () => {
arr.value = [4, 5, 6, 7]
}
</script>

<template>
<button @click="arrChange">change arr</button>
<button @click="objChange">change obj</button>

<div>arr:</div>
<div v-for="(item, index) in arr" :key="item">{{ item }}-{{ index }}</div>

<hr />

<div>obj:</div>
<div v-for="(item, index) in obj">{{ item }}-{{ index }}</div>

<hr />

<div>map:</div>
<div v-for="(item, index) in map" :key="item">{{ item }}-{{ index }}</div>

<hr />

<div>str:</div>
<div v-for="(item, index) in str" :key="item">{{ item }}-{{ index }}</div>

<hr />

<div>num:</div>
<div v-for="(item, index) in num" :key="item">{{ item }}-{{ index }}</div>
</template>
Loading