mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): createSelector (#279)
This commit is contained in:
parent
884c190f08
commit
e07eac9ba3
|
|
@ -3,9 +3,8 @@ import {
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
triggerRef,
|
triggerRef,
|
||||||
watch,
|
|
||||||
type ShallowRef,
|
type ShallowRef,
|
||||||
type WatchSource,
|
createSelector,
|
||||||
} from '@vue/vapor'
|
} from '@vue/vapor'
|
||||||
import { buildData } from './data'
|
import { buildData } from './data'
|
||||||
import { defer, wrap } from './profiling'
|
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)
|
const isSelected = createSelector(selected)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -113,7 +102,6 @@ const isSelected = createSelector(selected)
|
||||||
v-for="row of rows"
|
v-for="row of rows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:class="{ danger: isSelected(row.id) }"
|
:class="{ danger: isSelected(row.id) }"
|
||||||
v-memo="[row.label, row.id === selected]"
|
|
||||||
>
|
>
|
||||||
<td>{{ row.id }}</td>
|
<td>{{ row.id }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
renderEffect,
|
renderEffect,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
template,
|
template,
|
||||||
withDestructure,
|
|
||||||
withDirectives,
|
withDirectives,
|
||||||
} from '../src'
|
} from '../src'
|
||||||
import { makeRender } from './_utils'
|
import { makeRender } from './_utils'
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -132,6 +132,7 @@ export {
|
||||||
export { createIf } from './apiCreateIf'
|
export { createIf } from './apiCreateIf'
|
||||||
export { createFor, createForSlots } from './apiCreateFor'
|
export { createFor, createForSlots } from './apiCreateFor'
|
||||||
export { createComponent } from './apiCreateComponent'
|
export { createComponent } from './apiCreateComponent'
|
||||||
|
export { createSelector } from './apiCreateSelector'
|
||||||
|
|
||||||
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
|
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
|
||||||
export { toHandlers } from './helpers/toHandlers'
|
export { toHandlers } from './helpers/toHandlers'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue