feat(runtime-vapor): createSelector (#279)

This commit is contained in:
Rizumu Ayaka 2024-09-22 02:30:21 +08:00 committed by GitHub
parent 884c190f08
commit e07eac9ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 156 additions and 14 deletions

View File

@ -3,9 +3,8 @@ import {
ref,
shallowRef,
triggerRef,
watch,
type ShallowRef,
type WatchSource,
createSelector,
} from '@vue/vapor'
import { buildData } from './data'
import { defer, wrap } from './profiling'
@ -79,16 +78,6 @@ async function bench() {
}
}
// Reduce the complexity of `selected` from O(n) to O(1).
function createSelector(source: WatchSource) {
const cache: Record<keyof any, ShallowRef<boolean>> = {}
watch(source, (val, old) => {
if (old != undefined) cache[old]!.value = false
if (val != undefined) cache[val]!.value = true
})
return (id: keyof any) => (cache[id] ??= shallowRef(false)).value
}
const isSelected = createSelector(selected)
</script>
@ -113,7 +102,6 @@ const isSelected = createSelector(selected)
v-for="row of rows"
:key="row.id"
:class="{ danger: isSelected(row.id) }"
v-memo="[row.label, row.id === selected]"
>
<td>{{ row.id }}</td>
<td>

View File

@ -0,0 +1,112 @@
import { ref } from '@vue/reactivity'
import { makeRender } from './_utils'
import { createFor, createSelector, nextTick, renderEffect } from '../src'
const define = makeRender()
describe('api: createSelector', () => {
test('basic', async () => {
let calledTimes = 0
let expectedCalledTimes = 0
const list = ref([{ id: 0 }, { id: 1 }, { id: 2 }])
const index = ref(0)
const { host } = define(() => {
const isSleected = createSelector(index)
return createFor(
() => list.value,
([item]) => {
const span = document.createElement('li')
renderEffect(() => {
calledTimes += 1
const { id } = item.value
span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
})
return span
},
item => item.id,
)
}).render()
expect(host.innerHTML).toBe(
'<li>0.t</li><li>1.f</li><li>2.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 3))
index.value = 1
await nextTick()
expect(host.innerHTML).toBe(
'<li>0.f</li><li>1.t</li><li>2.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 2))
index.value = 2
await nextTick()
expect(host.innerHTML).toBe(
'<li>0.f</li><li>1.f</li><li>2.t</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 2))
list.value[2].id = 3
await nextTick()
expect(host.innerHTML).toBe(
'<li>0.f</li><li>1.f</li><li>3.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 1))
})
test('custom compare', async () => {
let calledTimes = 0
let expectedCalledTimes = 0
const list = ref([{ id: 1 }, { id: 2 }, { id: 3 }])
const index = ref(0)
const { host } = define(() => {
const isSleected = createSelector(
index,
(key, value) => key === value + 1,
)
return createFor(
() => list.value,
([item]) => {
const span = document.createElement('li')
renderEffect(() => {
calledTimes += 1
const { id } = item.value
span.textContent = `${id}.${isSleected(id) ? 't' : 'f'}`
})
return span
},
item => item.id,
)
}).render()
expect(host.innerHTML).toBe(
'<li>1.t</li><li>2.f</li><li>3.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 3))
index.value = 1
await nextTick()
expect(host.innerHTML).toBe(
'<li>1.f</li><li>2.t</li><li>3.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 2))
index.value = 2
await nextTick()
expect(host.innerHTML).toBe(
'<li>1.f</li><li>2.f</li><li>3.t</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 2))
list.value[2].id = 4
await nextTick()
expect(host.innerHTML).toBe(
'<li>1.f</li><li>2.f</li><li>4.f</li><!--for-->',
)
expect(calledTimes).toBe((expectedCalledTimes += 1))
})
})

View File

@ -7,7 +7,6 @@ import {
renderEffect,
shallowRef,
template,
withDestructure,
withDirectives,
} from '../src'
import { makeRender } from './_utils'

View File

@ -0,0 +1,42 @@
import {
type MaybeRefOrGetter,
type ShallowRef,
onScopeDispose,
shallowRef,
toValue,
} from '@vue/reactivity'
import { watchEffect } from './apiWatch'
export function createSelector<T, U extends T>(
source: MaybeRefOrGetter<T>,
fn: (key: U, value: T) => boolean = (key, value) => key === value,
): (key: U) => boolean {
let subs = new Map()
let val: T
let oldVal: U
watchEffect(() => {
val = toValue(source)
const keys = [...subs.keys()]
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i]
if (fn(key, val)) {
const o = subs.get(key)
o.value = true
} else if (oldVal !== undefined && fn(key, oldVal)) {
const o = subs.get(key)
o.value = false
}
}
oldVal = val as U
})
return key => {
let l: ShallowRef<boolean | undefined> & { _count?: number }
if (!(l = subs.get(key))) subs.set(key, (l = shallowRef()))
l.value
l._count ? l._count++ : (l._count = 1)
onScopeDispose(() => (l._count! > 1 ? l._count!-- : subs.delete(key)))
return l.value !== undefined ? l.value : fn(key, val)
}
}

View File

@ -132,6 +132,7 @@ export {
export { createIf } from './apiCreateIf'
export { createFor, createForSlots } from './apiCreateFor'
export { createComponent } from './apiCreateComponent'
export { createSelector } from './apiCreateSelector'
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
export { toHandlers } from './helpers/toHandlers'