wip: watch
This commit is contained in:
parent
dde6c151e4
commit
6441db45c7
@ -15,12 +15,12 @@ export function computed<T, C = null>(
|
||||
let value: any = undefined
|
||||
const runner = effect(() => getter.call(context, context), {
|
||||
lazy: true,
|
||||
// mark effect as computed so that it gets priority during trigger
|
||||
computed: true,
|
||||
scheduler: () => {
|
||||
dirty = true
|
||||
}
|
||||
})
|
||||
// mark effect as computed so that it gets priority during trigger
|
||||
runner.computed = true
|
||||
const computedValue = {
|
||||
// expose effect so computed can be stopped
|
||||
effect: runner,
|
||||
|
@ -15,6 +15,7 @@ export interface ReactiveEffect {
|
||||
|
||||
export interface ReactiveEffectOptions {
|
||||
lazy?: boolean
|
||||
computed?: boolean
|
||||
scheduler?: Scheduler
|
||||
onTrack?: Debugger
|
||||
onTrigger?: Debugger
|
||||
@ -48,6 +49,7 @@ export function createReactiveEffect(
|
||||
effect.scheduler = options.scheduler
|
||||
effect.onTrack = options.onTrack
|
||||
effect.onTrigger = options.onTrigger
|
||||
effect.computed = options.computed
|
||||
effect.deps = []
|
||||
return effect
|
||||
}
|
||||
|
@ -24,13 +24,13 @@ import {
|
||||
DebuggerEvent
|
||||
} from './effect'
|
||||
|
||||
import { UnwrapBindings } from './value'
|
||||
import { UnwrapValues } from './value'
|
||||
|
||||
export { ReactiveEffect, ReactiveEffectOptions, DebuggerEvent }
|
||||
export { OperationTypes } from './operations'
|
||||
export { computed, ComputedValue } from './computed'
|
||||
export { lock, unlock } from './lock'
|
||||
export { value, isValue, Value, UnwrapBindings } from './value'
|
||||
export { value, isValue, Value, UnwrapValues } from './value'
|
||||
|
||||
const collectionTypes: Set<any> = new Set([Set, Map, WeakMap, WeakSet])
|
||||
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
|
||||
@ -44,7 +44,7 @@ const canObserve = (value: any): boolean => {
|
||||
)
|
||||
}
|
||||
|
||||
type ObservableFactory = <T>(target?: T) => UnwrapBindings<T>
|
||||
type ObservableFactory = <T>(target?: T) => UnwrapValues<T>
|
||||
|
||||
export const observable = ((target: any = {}): any => {
|
||||
// if trying to observe an immutable proxy, return the immutable version.
|
||||
|
@ -9,12 +9,34 @@ export interface Value<T> {
|
||||
value: T
|
||||
}
|
||||
|
||||
const convert = (val: any): any => (isObject(val) ? observable(val) : val)
|
||||
|
||||
export function value<T>(raw: T): Value<T> {
|
||||
raw = convert(raw)
|
||||
const v = {
|
||||
get value() {
|
||||
track(v, OperationTypes.GET, '')
|
||||
return raw
|
||||
},
|
||||
set value(newVal) {
|
||||
raw = convert(newVal)
|
||||
trigger(v, OperationTypes.SET, '')
|
||||
}
|
||||
}
|
||||
knownValues.add(v)
|
||||
return v
|
||||
}
|
||||
|
||||
export function isValue(v: any): v is Value<any> {
|
||||
return knownValues.has(v)
|
||||
}
|
||||
|
||||
type UnwrapValue<T, U = T> = T extends Value<infer V> ? V : T extends {} ? U : T
|
||||
|
||||
// A utility type that recursively unwraps value bindings nested inside an
|
||||
// observable object. Unfortunately TS cannot do recursive types, but this
|
||||
// should be enough for practical use cases...
|
||||
export type UnwrapBindings<T> = {
|
||||
export type UnwrapValues<T> = {
|
||||
[key in keyof T]: UnwrapValue<
|
||||
T[key],
|
||||
{
|
||||
@ -64,25 +86,3 @@ export type UnwrapBindings<T> = {
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
const convert = (val: any): any => (isObject(val) ? observable(val) : val)
|
||||
|
||||
export function value<T>(raw: T): Value<T> {
|
||||
raw = convert(raw)
|
||||
const v = {
|
||||
get value() {
|
||||
track(v, OperationTypes.GET, '')
|
||||
return raw
|
||||
},
|
||||
set value(newVal) {
|
||||
raw = convert(newVal)
|
||||
trigger(v, OperationTypes.SET, '')
|
||||
}
|
||||
}
|
||||
knownValues.add(v)
|
||||
return v
|
||||
}
|
||||
|
||||
export function isValue(v: any): boolean {
|
||||
return knownValues.has(v)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { VNode, normalizeVNode, VNodeChild } from './vnode'
|
||||
import { ReactiveEffect, UnwrapBindings, observable } from '@vue/observer'
|
||||
import { ReactiveEffect, UnwrapValues, observable } from '@vue/observer'
|
||||
import { isFunction, EMPTY_OBJ } from '@vue/shared'
|
||||
import { RenderProxyHandlers } from './componentProxy'
|
||||
import { ComponentPropsOptions, PropValidator } from './componentProps'
|
||||
@ -31,7 +31,7 @@ export interface ComponentOptions<
|
||||
RawProps = ComponentPropsOptions,
|
||||
RawBindings = Data | void,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
Bindings = UnwrapBindings<RawBindings>
|
||||
Bindings = UnwrapValues<RawBindings>
|
||||
> {
|
||||
props?: RawProps
|
||||
setup?: (props: Props) => RawBindings
|
||||
@ -75,6 +75,7 @@ export type ComponentInstance<P = Data, S = Data> = {
|
||||
next: VNode | null
|
||||
subTree: VNode
|
||||
update: ReactiveEffect
|
||||
effects: ReactiveEffect[] | null
|
||||
// the rest are only for stateful components
|
||||
proxy: ComponentPublicProperties | null
|
||||
state: S
|
||||
@ -89,7 +90,7 @@ export function createComponent<
|
||||
RawProps,
|
||||
RawBindings,
|
||||
Props = ExtractPropTypes<RawProps>,
|
||||
Bindings = UnwrapBindings<RawBindings>
|
||||
Bindings = UnwrapValues<RawBindings>
|
||||
>(
|
||||
options: ComponentOptions<RawProps, RawBindings, Props, Bindings>
|
||||
): {
|
||||
@ -119,6 +120,7 @@ export function createComponentInstance(type: any): ComponentInstance {
|
||||
rtg: null,
|
||||
rtc: null,
|
||||
ec: null,
|
||||
effects: null,
|
||||
|
||||
// public properties
|
||||
state: EMPTY_OBJ,
|
||||
|
@ -6,13 +6,8 @@ function injectHook(
|
||||
target: ComponentInstance | null | void = currentInstance
|
||||
) {
|
||||
if (target) {
|
||||
const existing = target[name]
|
||||
// TODO inject a error-handling wrapped version of the hook
|
||||
if (existing !== null) {
|
||||
existing.push(hook)
|
||||
} else {
|
||||
target[name] = [hook]
|
||||
}
|
||||
;(target[name] || (target[name] = [])).push(hook)
|
||||
} else {
|
||||
// TODO warn
|
||||
}
|
||||
|
@ -769,17 +769,7 @@ export function createRenderer(options: RendererOptions) {
|
||||
function unmount(vnode: VNode, doRemove?: boolean) {
|
||||
const instance = vnode.component
|
||||
if (instance != null) {
|
||||
// beforeUnmount hook
|
||||
if (instance.bum !== null) {
|
||||
invokeHooks(instance.bum)
|
||||
}
|
||||
// TODO teardown component
|
||||
stop(instance.update)
|
||||
unmount(instance.subTree, doRemove)
|
||||
// unmounted hook
|
||||
if (instance.um !== null) {
|
||||
queuePostFlushCb(instance.um)
|
||||
}
|
||||
unmountComponent(instance, doRemove)
|
||||
return
|
||||
}
|
||||
const shouldRemoveChildren = vnode.type === Fragment && doRemove
|
||||
@ -794,6 +784,27 @@ export function createRenderer(options: RendererOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function unmountComponent(
|
||||
{ bum, effects, update, subTree, um }: ComponentInstance,
|
||||
doRemove?: boolean
|
||||
) {
|
||||
// beforeUnmount hook
|
||||
if (bum !== null) {
|
||||
invokeHooks(bum)
|
||||
}
|
||||
if (effects !== null) {
|
||||
for (let i = 0; i < effects.length; i++) {
|
||||
stop(effects[i])
|
||||
}
|
||||
}
|
||||
stop(update)
|
||||
unmount(subTree, doRemove)
|
||||
// unmounted hook
|
||||
if (um !== null) {
|
||||
queuePostFlushCb(um)
|
||||
}
|
||||
}
|
||||
|
||||
function unmountChildren(
|
||||
children: VNode[],
|
||||
doRemove?: boolean,
|
||||
|
@ -19,4 +19,4 @@ export * from './componentLifecycle'
|
||||
|
||||
export { createRenderer, RendererOptions } from './createRenderer'
|
||||
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
|
||||
export * from '@vue/observer'
|
||||
export * from './reactivity'
|
||||
|
136
packages/runtime-core/src/reactivity.ts
Normal file
136
packages/runtime-core/src/reactivity.ts
Normal file
@ -0,0 +1,136 @@
|
||||
export {
|
||||
value,
|
||||
isValue,
|
||||
observable,
|
||||
immutable,
|
||||
isObservable,
|
||||
isImmutable,
|
||||
unwrap,
|
||||
markImmutable,
|
||||
markNonReactive,
|
||||
effect,
|
||||
// types
|
||||
ReactiveEffect,
|
||||
ReactiveEffectOptions,
|
||||
DebuggerEvent,
|
||||
OperationTypes,
|
||||
Value,
|
||||
ComputedValue,
|
||||
UnwrapValues
|
||||
} from '@vue/observer'
|
||||
|
||||
import {
|
||||
effect,
|
||||
stop,
|
||||
computed as _computed,
|
||||
isValue,
|
||||
Value,
|
||||
ComputedValue,
|
||||
ReactiveEffect,
|
||||
ReactiveEffectOptions
|
||||
} from '@vue/observer'
|
||||
import { currentInstance } from './component'
|
||||
import { queueJob, queuePostFlushCb } from './scheduler'
|
||||
import { EMPTY_OBJ, isObject, isArray } from '@vue/shared'
|
||||
|
||||
function recordEffect(effect: ReactiveEffect) {
|
||||
if (currentInstance) {
|
||||
;(currentInstance.effects || (currentInstance.effects = [])).push(effect)
|
||||
}
|
||||
}
|
||||
|
||||
// a wrapped version of raw computed to tear it down at component unmount
|
||||
export function computed<T, C = null>(
|
||||
getter: (this: C, ctx: C) => T,
|
||||
context?: C
|
||||
): ComputedValue<T> {
|
||||
const c = _computed(getter, context)
|
||||
recordEffect(c.effect)
|
||||
return c
|
||||
}
|
||||
|
||||
export interface WatchOptions {
|
||||
lazy?: boolean
|
||||
flush?: 'pre' | 'post' | 'sync'
|
||||
deep?: boolean
|
||||
onTrack?: ReactiveEffectOptions['onTrack']
|
||||
onTrigger?: ReactiveEffectOptions['onTrigger']
|
||||
}
|
||||
|
||||
const invoke = (fn: Function) => fn()
|
||||
|
||||
export function watch<T>(
|
||||
source: Value<T> | (() => T),
|
||||
cb?: <V extends T>(newValue: V, oldValue: V) => (() => void) | void,
|
||||
options: WatchOptions = EMPTY_OBJ
|
||||
): () => void {
|
||||
const scheduler =
|
||||
options.flush === 'sync'
|
||||
? invoke
|
||||
: options.flush === 'pre'
|
||||
? queueJob
|
||||
: queuePostFlushCb
|
||||
|
||||
const traverseIfDeep = (getter: Function) =>
|
||||
options.deep ? () => traverse(getter()) : getter
|
||||
const getter = isValue(source)
|
||||
? traverseIfDeep(() => source.value)
|
||||
: traverseIfDeep(source)
|
||||
|
||||
let oldValue: any
|
||||
const applyCb = cb
|
||||
? () => {
|
||||
const newValue = runner()
|
||||
if (options.deep || newValue !== oldValue) {
|
||||
try {
|
||||
cb(newValue, oldValue)
|
||||
} catch (e) {
|
||||
// TODO handle error
|
||||
// handleError(e, instance, ErrorTypes.WATCH_CALLBACK)
|
||||
}
|
||||
oldValue = newValue
|
||||
}
|
||||
}
|
||||
: void 0
|
||||
|
||||
const runner = effect(getter, {
|
||||
lazy: true,
|
||||
// so it runs before component update effects in pre flush mode
|
||||
computed: true,
|
||||
onTrack: options.onTrack,
|
||||
onTrigger: options.onTrigger,
|
||||
scheduler: applyCb ? () => scheduler(applyCb) : void 0
|
||||
})
|
||||
|
||||
if (!options.lazy) {
|
||||
applyCb && scheduler(applyCb)
|
||||
} else {
|
||||
oldValue = runner()
|
||||
}
|
||||
|
||||
recordEffect(runner)
|
||||
return () => {
|
||||
stop(runner)
|
||||
}
|
||||
}
|
||||
|
||||
function traverse(value: any, seen: Set<any> = new Set()) {
|
||||
if (!isObject(value) || seen.has(value)) {
|
||||
return
|
||||
}
|
||||
seen.add(value)
|
||||
if (isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
traverse(value[i], seen)
|
||||
}
|
||||
} else if (value instanceof Map || value instanceof Set) {
|
||||
;(value as any).forEach((v: any) => {
|
||||
traverse(v, seen)
|
||||
})
|
||||
} else {
|
||||
for (const key in value) {
|
||||
traverse(value[key], seen)
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
@ -78,7 +78,7 @@ export function createVNode(
|
||||
type,
|
||||
props,
|
||||
key: props && props.key,
|
||||
children,
|
||||
children: typeof children === 'number' ? children + '' : children,
|
||||
component: null,
|
||||
el: null,
|
||||
anchor: null,
|
||||
|
Loading…
Reference in New Issue
Block a user