Skip to content

Commit

Permalink
wip(vca): partial ref and watch implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed May 24, 2022
1 parent d7d3dbb commit f50a1b8
Show file tree
Hide file tree
Showing 18 changed files with 1,066 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/composition-api/apiInject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function provide() {}

export function inject() {}
1 change: 1 addition & 0 deletions src/composition-api/apiLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function onMounted() {}
366 changes: 366 additions & 0 deletions src/composition-api/apiWatch.ts
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
}
12 changes: 12 additions & 0 deletions src/composition-api/currentInstance.ts
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
}
Loading

0 comments on commit f50a1b8

Please sign in to comment.