-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip(vca): partial ref and watch implementation
- Loading branch information
Showing
18 changed files
with
1,066 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function provide() {} | ||
|
||
export function inject() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export function onMounted() {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.