mirror of
https://github.com/vuejs/vue.git
synced 2024-11-21 20:28:54 +00:00
wip(vca): partial ref and watch implementation
This commit is contained in:
parent
d7d3dbbcc8
commit
f50a1b84d9
3
src/composition-api/apiInject.ts
Normal file
3
src/composition-api/apiInject.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function provide() {}
|
||||
|
||||
export function inject() {}
|
1
src/composition-api/apiLifecycle.ts
Normal file
1
src/composition-api/apiLifecycle.ts
Normal file
@ -0,0 +1 @@
|
||||
export function onMounted() {}
|
366
src/composition-api/apiWatch.ts
Normal file
366
src/composition-api/apiWatch.ts
Normal 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
|
||||
}
|
12
src/composition-api/currentInstance.ts
Normal file
12
src/composition-api/currentInstance.ts
Normal 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
|
||||
}
|
25
src/composition-api/index.ts
Normal file
25
src/composition-api/index.ts
Normal 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'
|
20
src/composition-api/reactivity/computed.ts
Normal file
20
src/composition-api/reactivity/computed.ts
Normal 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>
|
||||
}
|
54
src/composition-api/reactivity/effect.ts
Normal file
54
src/composition-api/reactivity/effect.ts
Normal 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
|
||||
}
|
||||
}
|
15
src/composition-api/reactivity/operations.ts
Normal file
15
src/composition-api/reactivity/operations.ts
Normal 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'
|
||||
}
|
12
src/composition-api/reactivity/reactive.ts
Normal file
12
src/composition-api/reactivity/reactive.ts
Normal 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__)
|
||||
}
|
143
src/composition-api/reactivity/ref.ts
Normal file
143
src/composition-api/reactivity/ref.ts
Normal 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
|
5
src/platforms/web/entry-runtime-esm.ts
Normal file
5
src/platforms/web/entry-runtime-esm.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Vue from './runtime/index'
|
||||
|
||||
export default Vue
|
||||
|
||||
export * from 'vca/index'
|
5
src/platforms/web/entry-runtime-with-compiler-esm.ts
Normal file
5
src/platforms/web/entry-runtime-with-compiler-esm.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Vue from './entry-runtime-with-compiler'
|
||||
|
||||
export default Vue
|
||||
|
||||
export * from 'vca/index'
|
@ -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
|
||||
|
@ -29,6 +29,7 @@
|
||||
"shared/*": ["../src/shared/*"],
|
||||
|
||||
"web/*": ["../src/platforms/web/*"],
|
||||
"vca/*": ["../src/composition-api/*"],
|
||||
|
||||
"vue": ["../src/platforms/web/entry-runtime-with-compiler"]
|
||||
}
|
||||
|
395
test/unit/features/composition-api/reactivity/ref.spec.ts
Normal file
395
test/unit/features/composition-api/reactivity/ref.spec.ts
Normal 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)
|
||||
// })
|
||||
})
|
@ -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
3
typescript/utils.ts
Normal 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
|
@ -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')
|
||||
}
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user