wip(vca): partial ref and watch implementation

This commit is contained in:
Evan You 2022-05-24 21:49:16 +08:00
parent d7d3dbbcc8
commit f50a1b84d9
18 changed files with 1066 additions and 0 deletions

View File

@ -0,0 +1,3 @@
export function provide() {}
export function inject() {}

View File

@ -0,0 +1 @@
export function onMounted() {}

View File

@ -0,0 +1,366 @@
import { isRef, Ref } from './reactivity/ref'
import { ComputedRef } from './reactivity/computed'
import { isReactive, isShallow } from './reactivity/reactive'
import { TrackOpTypes, TriggerOpTypes } from './reactivity/operations'
import {
warn,
noop,
isArray,
emptyObject,
remove,
isServerRendering,
invokeWithErrorHandling
} from 'core/util'
import { currentInstance } from './currentInstance'
import { traverse } from 'core/observer/traverse'
import { EffectScheduler, ReactiveEffect } from './reactivity/effect'
const WATCHER = `watcher`
const WATCHER_CB = `${WATCHER} callback`
const WATCHER_GETTER = `${WATCHER} getter`
const WATCHER_CLEANUP = `${WATCHER} cleanup`
export type WatchEffect = (onCleanup: OnCleanup) => void
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
export type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
type MapSources<T, Immediate> = {
[K in keyof T]: T[K] extends WatchSource<infer V>
? Immediate extends true
? V | undefined
: V
: T[K] extends object
? Immediate extends true
? T[K] | undefined
: T[K]
: never
}
type OnCleanup = (cleanupFn: () => void) => void
export interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
export type DebuggerEvent = {
// TODO effect: ReactiveEffect
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
export type WatchStopHandle = () => void
// Simple effect.
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'post' }
: { flush: 'post' }) as WatchOptionsBase
)
}
export function watchSyncEffect(
effect: WatchEffect,
options?: DebuggerOptions
) {
return doWatch(
effect,
null,
(__DEV__
? { ...options, flush: 'sync' }
: { flush: 'sync' }) as WatchOptionsBase
)
}
// initial value for watchers to trigger on undefined initial values
const INITIAL_WATCHER_VALUE = {}
type MultiWatchSources = (WatchSource<unknown> | object)[]
// overload: array of multiple sources + cb
export function watch<
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: multiple sources w/ `as const`
// watch([foo, bar] as const, () => {})
// somehow [...T] breaks when the type is readonly
export function watch<
T extends Readonly<MultiWatchSources>,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: single source + cb
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// overload: watching reactive object w/ cb
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && typeof cb !== 'function') {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
return doWatch(source as any, cb, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = emptyObject
): WatchStopHandle {
if (__DEV__ && !cb) {
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
if (deep !== undefined) {
warn(
`watch() "deep" option is only respected when using the ` +
`watch(source, callback, options?) signature.`
)
}
}
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
const instance = currentInstance
const call = (fn: Function, type: string, args: any[] | null = null) =>
invokeWithErrorHandling(fn, null, args, instance, type)
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => source
deep = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (typeof s === 'function') {
return call(s, WATCHER_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (typeof source === 'function') {
if (cb) {
// getter with cb
getter = () => call(source as Function, WATCHER_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return call(source as Function, WATCHER, [onCleanup])
}
}
} else {
getter = noop
__DEV__ && warnInvalidSource(source)
}
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
call(fn, WATCHER_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (isServerRendering()) {
// we will also not call the invalidate callback (+ runner is not set up)
onCleanup = noop
if (!cb) {
getter()
} else if (immediate) {
call(cb, WATCHER_CB, [
getter(),
isMultiSource ? [] : undefined,
onCleanup
])
}
return noop
}
let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
const job = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
call(cb, WATCHER_CB, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
let scheduler: EffectScheduler
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job)
} else {
// default: 'pre'
scheduler = () => queuePreFlushCb(job)
}
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(effect.run.bind(effect))
} else {
effect.run()
}
return () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill
function hasChanged(x: unknown, y: unknown): boolean {
if (x === y) {
return x !== 0 || 1 / x === 1 / (y as number)
} else {
return x !== x && y !== y
}
}
function queuePostRenderEffect(fn: Function) {
// TODO
}
function queuePreFlushCb(fn: Function) {
// TODO
}

View File

@ -0,0 +1,12 @@
import { Component } from 'typescript/component'
// TODO set this
export let currentInstance: Component | null = null
export function getCurrentInstance(): Component | null {
return currentInstance
}
export function setCurrentInstance(vm: Component | null) {
currentInstance = vm
}

View File

@ -0,0 +1,25 @@
export {
ref,
shallowRef,
isRef,
toRef,
toRefs,
unref,
customRef,
triggerRef,
Ref,
ToRef,
ToRefs,
UnwrapRef,
ShallowRef,
ShallowUnwrapRef,
RefUnwrapBailTypes,
CustomRefFactory
} from './reactivity/ref'
export {
watch,
watchEffect,
watchPostEffect,
watchSyncEffect
} from './apiWatch'

View File

@ -0,0 +1,20 @@
import { Ref } from './ref'
declare const ComputedRefSymbol: unique symbol
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
[ComputedRefSymbol]: true
}
export interface WritableComputedRef<T> extends Ref<T> {
// TODO readonly effect: ReactiveEffect<T>
}
export type ComputedGetter<T> = (...args: any[]) => T
export type ComputedSetter<T> = (v: T) => void
export interface WritableComputedOptions<T> {
get: ComputedGetter<T>
set: ComputedSetter<T>
}

View File

@ -0,0 +1,54 @@
import Watcher from 'core/observer/watcher'
import { noop } from 'shared/util'
import { currentInstance } from '../currentInstance'
import { TrackOpTypes, TriggerOpTypes } from './operations'
export type EffectScheduler = (...args: any[]) => any
export type DebuggerEvent = {
effect: ReactiveEffect
} & DebuggerEventExtraInfo
export type DebuggerEventExtraInfo = {
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
export class ReactiveEffect<T = any> {
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
public active = true
private _watcher: Watcher
constructor(
public fn: () => T,
public scheduler?: EffectScheduler // TODO scope?: EffectScope
) {
// TODO recordEffectScope(this, scope)
// TODO debug options
this._watcher = new Watcher(currentInstance, fn, noop, {
// force cb trigger
deep: true,
sync: !scheduler,
scheduler
})
}
run() {
this._watcher.run()
return this._watcher.value
}
stop() {
this._watcher.teardown()
this.active = false
}
}

View File

@ -0,0 +1,15 @@
// using literal strings instead of numbers so that it's easier to inspect
// debugger events
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}

View File

@ -0,0 +1,12 @@
export declare const ShallowReactiveMarker: unique symbol
export function reactive() {}
export function isReactive(value: unknown): boolean {
return !!(value && (value as any).__ob__)
}
export function isShallow(value: unknown): boolean {
// TODO
return !!(value && (value as any).__ob__)
}

View File

@ -0,0 +1,143 @@
import { defineReactive } from 'core/observer/index'
import type { ShallowReactiveMarker } from './reactive'
import type { IfAny } from 'typescript/utils'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
export interface Ref<T = any> {
value: T
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true
}
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
export function ref<T extends object>(
value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
export function shallowRef<T extends object>(
value: T
): T extends Ref ? T : ShallowRef<T>
export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
const ref = { __v_isRef: true }
defineReactive(ref, 'value', rawValue, null, shallow)
return ref
}
export function triggerRef(ref: Ref) {
// TODO triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? (ref.value as any) : ref
}
export type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
export function customRef() {
// TODO
}
export type ToRefs<T = any> = {
[K in keyof T]: ToRef<T[K]>
}
export function toRefs() {
// TODO
}
export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>
export function toRef() {
// TODO
}
/**
* This is a special exported interface for other packages to declare
* additional types that should bail out for ref unwrapping. For example
* \@vue/runtime-dom can declare it like so in its d.ts:
*
* ``` ts
* declare module 'vue' {
* export interface RefUnwrapBailTypes {
* runtimeDOMBailTypes: Node | Window
* }
* }
* ```
*
* Note that api-extractor somehow refuses to include `declare module`
* augmentations in its generated d.ts, so we have to manually append them
* to the final generated d.ts in our build process.
*/
export interface RefUnwrapBailTypes {}
export type ShallowUnwrapRef<T> = {
[K in keyof T]: T[K] extends Ref<infer V>
? V
: // if `V` is `unknown` that means it does not extend `Ref` and is undefined
T[K] extends Ref<infer V> | undefined
? unknown extends V
? undefined
: V | undefined
: T[K]
}
export type UnwrapRef<T> = T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
type BaseTypes = string | number | boolean
type CollectionTypes = IterableCollections | WeakCollections
type IterableCollections = Map<any, any> | Set<any>
type WeakCollections = WeakMap<any, any> | WeakSet<any>
export type UnwrapRefSimple<T> = T extends
| Function
| CollectionTypes
| BaseTypes
| Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }
? T
: T extends Array<any>
? { [K in keyof T]: UnwrapRefSimple<T[K]> }
: T extends object & { [ShallowReactiveMarker]?: never }
? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
}
: T

View File

@ -0,0 +1,5 @@
import Vue from './runtime/index'
export default Vue
export * from 'vca/index'

View File

@ -0,0 +1,5 @@
import Vue from './entry-runtime-with-compiler'
export default Vue
export * from 'vca/index'

View File

@ -1,3 +1,7 @@
import Vue from './runtime/index'
import * as vca from 'vca/index'
import { extend } from 'shared/util'
extend(Vue, vca)
export default Vue

View File

@ -29,6 +29,7 @@
"shared/*": ["../src/shared/*"],
"web/*": ["../src/platforms/web/*"],
"vca/*": ["../src/composition-api/*"],
"vue": ["../src/platforms/web/entry-runtime-with-compiler"]
}

View File

@ -0,0 +1,395 @@
import { ref, shallowRef, unref } from 'vca/reactivity/ref'
import { ReactiveEffect } from 'vca/reactivity/effect'
import { isReactive } from 'vca/reactivity/reactive'
const effect = (fn: () => any) => new ReactiveEffect(fn)
describe('reactivity/ref', () => {
it('should hold a value', () => {
const a = ref(1)
expect(a.value).toBe(1)
a.value = 2
expect(a.value).toBe(2)
})
it('should be reactive', () => {
const a = ref(1)
let dummy
let calls = 0
effect(() => {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// same value should not trigger
a.value = 2
expect(calls).toBe(2)
})
it('should make nested properties reactive', () => {
const a = ref({
count: 1
})
let dummy
effect(() => {
dummy = a.value.count
})
expect(dummy).toBe(1)
a.value.count = 2
expect(dummy).toBe(2)
})
it('should work without initial value', () => {
const a = ref()
let dummy
effect(() => {
dummy = a.value
})
expect(dummy).toBe(undefined)
a.value = 2
expect(dummy).toBe(2)
})
// it('should work like a normal property when nested in a reactive object', () => {
// const a = ref(1)
// const obj = reactive({
// a,
// b: {
// c: a
// }
// })
// let dummy1: number
// let dummy2: number
// effect(() => {
// dummy1 = obj.a
// dummy2 = obj.b.c
// })
// const assertDummiesEqualTo = (val: number) =>
// [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
// assertDummiesEqualTo(1)
// a.value++
// assertDummiesEqualTo(2)
// obj.a++
// assertDummiesEqualTo(3)
// obj.b.c++
// assertDummiesEqualTo(4)
// })
// it('should unwrap nested ref in types', () => {
// const a = ref(0)
// const b = ref(a)
// expect(typeof (b.value + 1)).toBe('number')
// })
// it('should unwrap nested values in types', () => {
// const a = {
// b: ref(0)
// }
// const c = ref(a)
// expect(typeof (c.value.b + 1)).toBe('number')
// })
// it('should NOT unwrap ref types nested inside arrays', () => {
// const arr = ref([1, ref(3)]).value
// expect(isRef(arr[0])).toBe(false)
// expect(isRef(arr[1])).toBe(true)
// expect((arr[1] as Ref).value).toBe(3)
// })
// it('should unwrap ref types as props of arrays', () => {
// const arr = [ref(0)]
// const symbolKey = Symbol('')
// arr['' as any] = ref(1)
// arr[symbolKey as any] = ref(2)
// const arrRef = ref(arr).value
// expect(isRef(arrRef[0])).toBe(true)
// expect(isRef(arrRef['' as any])).toBe(false)
// expect(isRef(arrRef[symbolKey as any])).toBe(false)
// expect(arrRef['' as any]).toBe(1)
// expect(arrRef[symbolKey as any]).toBe(2)
// })
// it('should keep tuple types', () => {
// const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
// 0,
// '1',
// { a: 1 },
// () => 0,
// ref(0)
// ]
// const tupleRef = ref(tuple)
// tupleRef.value[0]++
// expect(tupleRef.value[0]).toBe(1)
// tupleRef.value[1] += '1'
// expect(tupleRef.value[1]).toBe('11')
// tupleRef.value[2].a++
// expect(tupleRef.value[2].a).toBe(2)
// expect(tupleRef.value[3]()).toBe(0)
// tupleRef.value[4].value++
// expect(tupleRef.value[4].value).toBe(1)
// })
// it('should keep symbols', () => {
// const customSymbol = Symbol()
// const obj = {
// [Symbol.asyncIterator]: ref(1),
// [Symbol.hasInstance]: { a: ref('a') },
// [Symbol.isConcatSpreadable]: { b: ref(true) },
// [Symbol.iterator]: [ref(1)],
// [Symbol.match]: new Set<Ref<number>>(),
// [Symbol.matchAll]: new Map<number, Ref<string>>(),
// [Symbol.replace]: { arr: [ref('a')] },
// [Symbol.search]: { set: new Set<Ref<number>>() },
// [Symbol.species]: { map: new Map<number, Ref<string>>() },
// [Symbol.split]: new WeakSet<Ref<boolean>>(),
// [Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
// [Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
// [Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
// [customSymbol]: { arr: [ref(1)] }
// }
// const objRef = ref(obj)
// const keys: (keyof typeof obj)[] = [
// Symbol.asyncIterator,
// Symbol.hasInstance,
// Symbol.isConcatSpreadable,
// Symbol.iterator,
// Symbol.match,
// Symbol.matchAll,
// Symbol.replace,
// Symbol.search,
// Symbol.species,
// Symbol.split,
// Symbol.toPrimitive,
// Symbol.toStringTag,
// Symbol.unscopables,
// customSymbol
// ]
// keys.forEach(key => {
// expect(objRef.value[key]).toStrictEqual(obj[key])
// })
// })
test('unref', () => {
expect(unref(1)).toBe(1)
expect(unref(ref(1))).toBe(1)
})
test('shallowRef', () => {
const sref = shallowRef({ a: 1 })
expect(isReactive(sref.value)).toBe(false)
let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)
sref.value = { a: 2 }
expect(isReactive(sref.value)).toBe(false)
expect(dummy).toBe(2)
})
// test('shallowRef force trigger', () => {
// const sref = shallowRef({ a: 1 })
// let dummy
// effect(() => {
// dummy = sref.value.a
// })
// expect(dummy).toBe(1)
// sref.value.a = 2
// expect(dummy).toBe(1) // should not trigger yet
// // force trigger
// triggerRef(sref)
// expect(dummy).toBe(2)
// })
// test('shallowRef isShallow', () => {
// expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
// })
// test('isRef', () => {
// expect(isRef(ref(1))).toBe(true)
// expect(isRef(computed(() => 1))).toBe(true)
// expect(isRef(0)).toBe(false)
// expect(isRef(1)).toBe(false)
// // an object that looks like a ref isn't necessarily a ref
// expect(isRef({ value: 0 })).toBe(false)
// })
// test('toRef', () => {
// const a = reactive({
// x: 1
// })
// const x = toRef(a, 'x')
// expect(isRef(x)).toBe(true)
// expect(x.value).toBe(1)
// // source -> proxy
// a.x = 2
// expect(x.value).toBe(2)
// // proxy -> source
// x.value = 3
// expect(a.x).toBe(3)
// // reactivity
// let dummyX
// effect(() => {
// dummyX = x.value
// })
// expect(dummyX).toBe(x.value)
// // mutating source should trigger effect using the proxy refs
// a.x = 4
// expect(dummyX).toBe(4)
// // should keep ref
// const r = { x: ref(1) }
// expect(toRef(r, 'x')).toBe(r.x)
// })
// test('toRef default value', () => {
// const a: { x: number | undefined } = { x: undefined }
// const x = toRef(a, 'x', 1)
// expect(x.value).toBe(1)
// a.x = 2
// expect(x.value).toBe(2)
// a.x = undefined
// expect(x.value).toBe(1)
// })
// test('toRefs', () => {
// const a = reactive({
// x: 1,
// y: 2
// })
// const { x, y } = toRefs(a)
// expect(isRef(x)).toBe(true)
// expect(isRef(y)).toBe(true)
// expect(x.value).toBe(1)
// expect(y.value).toBe(2)
// // source -> proxy
// a.x = 2
// a.y = 3
// expect(x.value).toBe(2)
// expect(y.value).toBe(3)
// // proxy -> source
// x.value = 3
// y.value = 4
// expect(a.x).toBe(3)
// expect(a.y).toBe(4)
// // reactivity
// let dummyX, dummyY
// effect(() => {
// dummyX = x.value
// dummyY = y.value
// })
// expect(dummyX).toBe(x.value)
// expect(dummyY).toBe(y.value)
// // mutating source should trigger effect using the proxy refs
// a.x = 4
// a.y = 5
// expect(dummyX).toBe(4)
// expect(dummyY).toBe(5)
// })
// test('toRefs should warn on plain object', () => {
// toRefs({})
// expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
// })
// test('toRefs should warn on plain array', () => {
// toRefs([])
// expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
// })
// test('toRefs reactive array', () => {
// const arr = reactive(['a', 'b', 'c'])
// const refs = toRefs(arr)
// expect(Array.isArray(refs)).toBe(true)
// refs[0].value = '1'
// expect(arr[0]).toBe('1')
// arr[1] = '2'
// expect(refs[1].value).toBe('2')
// })
// test('customRef', () => {
// let value = 1
// let _trigger: () => void
// const custom = customRef((track, trigger) => ({
// get() {
// track()
// return value
// },
// set(newValue: number) {
// value = newValue
// _trigger = trigger
// }
// }))
// expect(isRef(custom)).toBe(true)
// let dummy
// effect(() => {
// dummy = custom.value
// })
// expect(dummy).toBe(1)
// custom.value = 2
// // should not trigger yet
// expect(dummy).toBe(1)
// _trigger!()
// expect(dummy).toBe(2)
// })
// test('should not trigger when setting value to same proxy', () => {
// const obj = reactive({ count: 0 })
// const a = ref(obj)
// const spy1 = jest.fn(() => a.value)
// effect(spy1)
// a.value = obj
// expect(spy1).toBeCalledTimes(1)
// const b = shallowRef(obj)
// const spy2 = jest.fn(() => b.value)
// effect(spy2)
// b.value = obj
// expect(spy2).toBeCalledTimes(1)
// })
})

View File

@ -29,6 +29,7 @@
"shared/*": ["src/shared/*"],
"web/*": ["src/platforms/web/*"],
"vca/*": ["src/composition-api/*"],
"vue": ["src/platforms/web/entry-runtime-with-compiler"]
}

3
typescript/utils.ts Normal file
View File

@ -0,0 +1,3 @@
// If the the type T accepts type "any", output type Y, otherwise output type N.
// https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N

View File

@ -12,6 +12,7 @@ export default defineConfig({
sfc: resolve('src/sfc'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
vca: resolve('src/composition-api'),
vue: resolve('src/platforms/web/entry-runtime-with-compiler')
}
},