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,
 | 
			
		||||
  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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  shallowRef,
 | 
			
		||||
  template,
 | 
			
		||||
  withDestructure,
 | 
			
		||||
  withDirectives,
 | 
			
		||||
} from '../src'
 | 
			
		||||
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 { createFor, createForSlots } from './apiCreateFor'
 | 
			
		||||
export { createComponent } from './apiCreateComponent'
 | 
			
		||||
export { createSelector } from './apiCreateSelector'
 | 
			
		||||
 | 
			
		||||
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
 | 
			
		||||
export { toHandlers } from './helpers/toHandlers'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue