diff --git a/benchmark/client/App.vue b/benchmark/client/App.vue index 3ca56bdfb..0e7cbc82f 100644 --- a/benchmark/client/App.vue +++ b/benchmark/client/App.vue @@ -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> = {} - 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) @@ -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]" > {{ row.id }} diff --git a/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts new file mode 100644 index 000000000..fc1979ec5 --- /dev/null +++ b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts @@ -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( + '
  • 0.t
  • 1.f
  • 2.f
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 3)) + + index.value = 1 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0.f
  • 1.t
  • 2.f
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 2)) + + index.value = 2 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0.f
  • 1.f
  • 2.t
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 2)) + + list.value[2].id = 3 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0.f
  • 1.f
  • 3.f
  • ', + ) + 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( + '
  • 1.t
  • 2.f
  • 3.f
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 3)) + + index.value = 1 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 1.f
  • 2.t
  • 3.f
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 2)) + + index.value = 2 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 1.f
  • 2.f
  • 3.t
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 2)) + + list.value[2].id = 4 + await nextTick() + expect(host.innerHTML).toBe( + '
  • 1.f
  • 2.f
  • 4.f
  • ', + ) + expect(calledTimes).toBe((expectedCalledTimes += 1)) + }) +}) diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index 815d0eec1..8cb9f1c00 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -7,7 +7,6 @@ import { renderEffect, shallowRef, template, - withDestructure, withDirectives, } from '../src' import { makeRender } from './_utils' diff --git a/packages/runtime-vapor/src/apiCreateSelector.ts b/packages/runtime-vapor/src/apiCreateSelector.ts new file mode 100644 index 000000000..121c36bf6 --- /dev/null +++ b/packages/runtime-vapor/src/apiCreateSelector.ts @@ -0,0 +1,42 @@ +import { + type MaybeRefOrGetter, + type ShallowRef, + onScopeDispose, + shallowRef, + toValue, +} from '@vue/reactivity' +import { watchEffect } from './apiWatch' + +export function createSelector( + source: MaybeRefOrGetter, + 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 & { _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) + } +} diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 17c725c5e..1e49010f4 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -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'