ref,
shallowRef,
triggerRef,
- watch,
type ShallowRef,
- type WatchSource,
+ createSelector,
} from '@vue/vapor'
import { buildData } from './data'
import { defer, wrap } from './profiling'
}
}
-// 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>
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>
--- /dev/null
+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))
+ })
+})
--- /dev/null
+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)
+ }
+}