diff --git a/packages/observer/src/computed.ts b/packages/observer/src/computed.ts index 5a517f780..f91bfb472 100644 --- a/packages/observer/src/computed.ts +++ b/packages/observer/src/computed.ts @@ -15,12 +15,12 @@ export function computed( 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, diff --git a/packages/observer/src/effect.ts b/packages/observer/src/effect.ts index e149de048..37de5815c 100644 --- a/packages/observer/src/effect.ts +++ b/packages/observer/src/effect.ts @@ -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 } diff --git a/packages/observer/src/index.ts b/packages/observer/src/index.ts index f3aea4e6d..44669a798 100644 --- a/packages/observer/src/index.ts +++ b/packages/observer/src/index.ts @@ -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 = 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 = (target?: T) => UnwrapBindings +type ObservableFactory = (target?: T) => UnwrapValues export const observable = ((target: any = {}): any => { // if trying to observe an immutable proxy, return the immutable version. diff --git a/packages/observer/src/value.ts b/packages/observer/src/value.ts index c274eb026..4101bbd35 100644 --- a/packages/observer/src/value.ts +++ b/packages/observer/src/value.ts @@ -9,12 +9,34 @@ export interface Value { value: T } +const convert = (val: any): any => (isObject(val) ? observable(val) : val) + +export function value(raw: T): Value { + 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 { + return knownValues.has(v) +} + type UnwrapValue = T extends Value ? 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 = { +export type UnwrapValues = { [key in keyof T]: UnwrapValue< T[key], { @@ -64,25 +86,3 @@ export type UnwrapBindings = { } > } - -const convert = (val: any): any => (isObject(val) ? observable(val) : val) - -export function value(raw: T): Value { - 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) -} diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 99416c562..e2db5c110 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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, - Bindings = UnwrapBindings + Bindings = UnwrapValues > { props?: RawProps setup?: (props: Props) => RawBindings @@ -75,6 +75,7 @@ export type ComponentInstance

= { 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, - Bindings = UnwrapBindings + Bindings = UnwrapValues >( options: ComponentOptions ): { @@ -119,6 +120,7 @@ export function createComponentInstance(type: any): ComponentInstance { rtg: null, rtc: null, ec: null, + effects: null, // public properties state: EMPTY_OBJ, diff --git a/packages/runtime-core/src/componentLifecycle.ts b/packages/runtime-core/src/componentLifecycle.ts index 2b0ed9d1d..a0525e177 100644 --- a/packages/runtime-core/src/componentLifecycle.ts +++ b/packages/runtime-core/src/componentLifecycle.ts @@ -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 } diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index ba8ff88a2..60505a428 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -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, diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 9a40d995a..2069e2f9b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -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' diff --git a/packages/runtime-core/src/reactivity.ts b/packages/runtime-core/src/reactivity.ts new file mode 100644 index 000000000..493ae81c5 --- /dev/null +++ b/packages/runtime-core/src/reactivity.ts @@ -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( + getter: (this: C, ctx: C) => T, + context?: C +): ComputedValue { + 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( + source: Value | (() => T), + cb?: (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 = 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 +} diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 6432b9811..2b06f2248 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -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,