Skip to content

Commit e07eac9

Browse files
authored
feat(runtime-vapor): createSelector (#279)
1 parent 884c190 commit e07eac9

File tree

5 files changed

+156
-14
lines changed

5 files changed

+156
-14
lines changed

benchmark/client/App.vue

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import {
33
ref,
44
shallowRef,
55
triggerRef,
6-
watch,
76
type ShallowRef,
8-
type WatchSource,
7+
createSelector,
98
} from '@vue/vapor'
109
import { buildData } from './data'
1110
import { defer, wrap } from './profiling'
@@ -79,16 +78,6 @@ async function bench() {
7978
}
8079
}
8180
82-
// Reduce the complexity of `selected` from O(n) to O(1).
83-
function createSelector(source: WatchSource) {
84-
const cache: Record<keyof any, ShallowRef<boolean>> = {}
85-
watch(source, (val, old) => {
86-
if (old != undefined) cache[old]!.value = false
87-
if (val != undefined) cache[val]!.value = true
88-
})
89-
return (id: keyof any) => (cache[id] ??= shallowRef(false)).value
90-
}
91-
9281
const isSelected = createSelector(selected)
9382
</script>
9483

@@ -113,7 +102,6 @@ const isSelected = createSelector(selected)
113102
v-for="row of rows"
114103
:key="row.id"
115104
:class="{ danger: isSelected(row.id) }"
116-
v-memo="[row.label, row.id === selected]"
117105
>
118106
<td>{{ row.id }}</td>
119107
<td>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ref } from '@vue/reactivity'
2+
import { makeRender } from './_utils'
3+
import { createFor, createSelector, nextTick, renderEffect } from '../src'
4+
5+
const define = makeRender()
6+
7+
describe('api: createSelector', () => {
8+
test('basic', async () => {
9+
let calledTimes = 0
10+
let expectedCalledTimes = 0
11+
12+
const list = ref([{ id: 0 }, { id: 1 }, { id: 2 }])
13+
const index = ref(0)
14+
15+
const { host } = define(() => {
16+
const isSleected = createSelector(index)
17+
return createFor(
18+
() => list.value,
19+
([item]) => {
20+
const span = document.createElement('li')
21+
renderEffect(() => {
22+
calledTimes += 1
23+
const { id } = item.value
24+
span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
25+
})
26+
return span
27+
},
28+
item => item.id,
29+
)
30+
}).render()
31+
32+
expect(host.innerHTML).toBe(
33+
'<li>0.t</li><li>1.f</li><li>2.f</li><!--for-->',
34+
)
35+
expect(calledTimes).toBe((expectedCalledTimes += 3))
36+
37+
index.value = 1
38+
await nextTick()
39+
expect(host.innerHTML).toBe(
40+
'<li>0.f</li><li>1.t</li><li>2.f</li><!--for-->',
41+
)
42+
expect(calledTimes).toBe((expectedCalledTimes += 2))
43+
44+
index.value = 2
45+
await nextTick()
46+
expect(host.innerHTML).toBe(
47+
'<li>0.f</li><li>1.f</li><li>2.t</li><!--for-->',
48+
)
49+
expect(calledTimes).toBe((expectedCalledTimes += 2))
50+
51+
list.value[2].id = 3
52+
await nextTick()
53+
expect(host.innerHTML).toBe(
54+
'<li>0.f</li><li>1.f</li><li>3.f</li><!--for-->',
55+
)
56+
expect(calledTimes).toBe((expectedCalledTimes += 1))
57+
})
58+
59+
test('custom compare', async () => {
60+
let calledTimes = 0
61+
let expectedCalledTimes = 0
62+
63+
const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }])
64+
const index = ref(0)
65+
66+
const { host } = define(() => {
67+
const isSleected = createSelector(
68+
index,
69+
(key, value) => key === value + 1,
70+
)
71+
return createFor(
72+
() => list.value,
73+
([item]) => {
74+
const span = document.createElement('li')
75+
renderEffect(() => {
76+
calledTimes += 1
77+
const { id } = item.value
78+
span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
79+
})
80+
return span
81+
},
82+
item => item.id,
83+
)
84+
}).render()
85+
86+
expect(host.innerHTML).toBe(
87+
'<li>1.t</li><li>2.f</li><li>3.f</li><!--for-->',
88+
)
89+
expect(calledTimes).toBe((expectedCalledTimes += 3))
90+
91+
index.value = 1
92+
await nextTick()
93+
expect(host.innerHTML).toBe(
94+
'<li>1.f</li><li>2.t</li><li>3.f</li><!--for-->',
95+
)
96+
expect(calledTimes).toBe((expectedCalledTimes += 2))
97+
98+
index.value = 2
99+
await nextTick()
100+
expect(host.innerHTML).toBe(
101+
'<li>1.f</li><li>2.f</li><li>3.t</li><!--for-->',
102+
)
103+
expect(calledTimes).toBe((expectedCalledTimes += 2))
104+
105+
list.value[2].id = 4
106+
await nextTick()
107+
expect(host.innerHTML).toBe(
108+
'<li>1.f</li><li>2.f</li><li>4.f</li><!--for-->',
109+
)
110+
expect(calledTimes).toBe((expectedCalledTimes += 1))
111+
})
112+
})

packages/runtime-vapor/__tests__/for.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
renderEffect,
88
shallowRef,
99
template,
10-
withDestructure,
1110
withDirectives,
1211
} from '../src'
1312
import { makeRender } from './_utils'
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
type MaybeRefOrGetter,
3+
type ShallowRef,
4+
onScopeDispose,
5+
shallowRef,
6+
toValue,
7+
} from '@vue/reactivity'
8+
import { watchEffect } from './apiWatch'
9+
10+
export function createSelector<T, U extends T>(
11+
source: MaybeRefOrGetter<T>,
12+
fn: (key: U, value: T) => boolean = (key, value) => key === value,
13+
): (key: U) => boolean {
14+
let subs = new Map()
15+
let val: T
16+
let oldVal: U
17+
18+
watchEffect(() => {
19+
val = toValue(source)
20+
const keys = [...subs.keys()]
21+
for (let i = 0, len = keys.length; i < len; i++) {
22+
const key = keys[i]
23+
if (fn(key, val)) {
24+
const o = subs.get(key)
25+
o.value = true
26+
} else if (oldVal !== undefined && fn(key, oldVal)) {
27+
const o = subs.get(key)
28+
o.value = false
29+
}
30+
}
31+
oldVal = val as U
32+
})
33+
34+
return key => {
35+
let l: ShallowRef<boolean | undefined> & { _count?: number }
36+
if (!(l = subs.get(key))) subs.set(key, (l = shallowRef()))
37+
l.value
38+
l._count ? l._count++ : (l._count = 1)
39+
onScopeDispose(() => (l._count! > 1 ? l._count!-- : subs.delete(key)))
40+
return l.value !== undefined ? l.value : fn(key, val)
41+
}
42+
}

packages/runtime-vapor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export {
132132
export { createIf } from './apiCreateIf'
133133
export { createFor, createForSlots } from './apiCreateFor'
134134
export { createComponent } from './apiCreateComponent'
135+
export { createSelector } from './apiCreateSelector'
135136

136137
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
137138
export { toHandlers } from './helpers/toHandlers'

0 commit comments

Comments
 (0)