]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): createSelector (#279)
authorRizumu Ayaka <rizumu@ayaka.moe>
Sat, 21 Sep 2024 18:30:21 +0000 (02:30 +0800)
committerGitHub <noreply@github.com>
Sat, 21 Sep 2024 18:30:21 +0000 (02:30 +0800)
benchmark/client/App.vue
packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts [new file with mode: 0644]
packages/runtime-vapor/__tests__/for.spec.ts
packages/runtime-vapor/src/apiCreateSelector.ts [new file with mode: 0644]
packages/runtime-vapor/src/index.ts

index 3ca56bdfb1ae183fa621b5dc5e8a3a9f97d0d7ba..0e7cbc82fbd5a7eb05e21b15f6e3f40fcd7bc318 100644 (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>
diff --git a/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts
new file mode 100644 (file)
index 0000000..fc1979e
--- /dev/null
@@ -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))
+  })
+})
index 815d0eec1519fbd5090dd52f43f5732bc14be782..8cb9f1c0020343ac70f1ac8fb7126327c4af05a7 100644 (file)
@@ -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 (file)
index 0000000..121c36b
--- /dev/null
@@ -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)
+  }
+}
index 17c725c5e7c21c0e71fa1f5dafe838739f7a2164..1e49010f43debc1d2048f360dfe0d882e3dcd1e7 100644 (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'