wip: watch

This commit is contained in:
Evan You 2019-05-29 23:44:59 +08:00
parent dde6c151e4
commit 6441db45c7
10 changed files with 196 additions and 50 deletions

View File

@ -15,12 +15,12 @@ export function computed<T, C = null>(
let value: any = undefined let value: any = undefined
const runner = effect(() => getter.call(context, context), { const runner = effect(() => getter.call(context, context), {
lazy: true, lazy: true,
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => { scheduler: () => {
dirty = true dirty = true
} }
}) })
// mark effect as computed so that it gets priority during trigger
runner.computed = true
const computedValue = { const computedValue = {
// expose effect so computed can be stopped // expose effect so computed can be stopped
effect: runner, effect: runner,

View File

@ -15,6 +15,7 @@ export interface ReactiveEffect {
export interface ReactiveEffectOptions { export interface ReactiveEffectOptions {
lazy?: boolean lazy?: boolean
computed?: boolean
scheduler?: Scheduler scheduler?: Scheduler
onTrack?: Debugger onTrack?: Debugger
onTrigger?: Debugger onTrigger?: Debugger
@ -48,6 +49,7 @@ export function createReactiveEffect(
effect.scheduler = options.scheduler effect.scheduler = options.scheduler
effect.onTrack = options.onTrack effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger effect.onTrigger = options.onTrigger
effect.computed = options.computed
effect.deps = [] effect.deps = []
return effect return effect
} }

View File

@ -24,13 +24,13 @@ import {
DebuggerEvent DebuggerEvent
} from './effect' } from './effect'
import { UnwrapBindings } from './value' import { UnwrapValues } from './value'
export { ReactiveEffect, ReactiveEffectOptions, DebuggerEvent } export { ReactiveEffect, ReactiveEffectOptions, DebuggerEvent }
export { OperationTypes } from './operations' export { OperationTypes } from './operations'
export { computed, ComputedValue } from './computed' export { computed, ComputedValue } from './computed'
export { lock, unlock } from './lock' 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 collectionTypes: Set<any> = new Set([Set, Map, WeakMap, WeakSet])
const observableValueRE = /^\[object (?:Object|Array|Map|Set|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 => { export const observable = ((target: any = {}): any => {
// if trying to observe an immutable proxy, return the immutable version. // if trying to observe an immutable proxy, return the immutable version.

View File

@ -9,12 +9,34 @@ export interface Value<T> {
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 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 // A utility type that recursively unwraps value bindings nested inside an
// observable object. Unfortunately TS cannot do recursive types, but this // observable object. Unfortunately TS cannot do recursive types, but this
// should be enough for practical use cases... // should be enough for practical use cases...
export type UnwrapBindings<T> = { export type UnwrapValues<T> = {
[key in keyof T]: UnwrapValue< [key in keyof T]: UnwrapValue<
T[key], 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)
}

View File

@ -1,5 +1,5 @@
import { VNode, normalizeVNode, VNodeChild } from './vnode' 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 { isFunction, EMPTY_OBJ } from '@vue/shared'
import { RenderProxyHandlers } from './componentProxy' import { RenderProxyHandlers } from './componentProxy'
import { ComponentPropsOptions, PropValidator } from './componentProps' import { ComponentPropsOptions, PropValidator } from './componentProps'
@ -31,7 +31,7 @@ export interface ComponentOptions<
RawProps = ComponentPropsOptions, RawProps = ComponentPropsOptions,
RawBindings = Data | void, RawBindings = Data | void,
Props = ExtractPropTypes<RawProps>, Props = ExtractPropTypes<RawProps>,
Bindings = UnwrapBindings<RawBindings> Bindings = UnwrapValues<RawBindings>
> { > {
props?: RawProps props?: RawProps
setup?: (props: Props) => RawBindings setup?: (props: Props) => RawBindings
@ -75,6 +75,7 @@ export type ComponentInstance<P = Data, S = Data> = {
next: VNode | null next: VNode | null
subTree: VNode subTree: VNode
update: ReactiveEffect update: ReactiveEffect
effects: ReactiveEffect[] | null
// the rest are only for stateful components // the rest are only for stateful components
proxy: ComponentPublicProperties | null proxy: ComponentPublicProperties | null
state: S state: S
@ -89,7 +90,7 @@ export function createComponent<
RawProps, RawProps,
RawBindings, RawBindings,
Props = ExtractPropTypes<RawProps>, Props = ExtractPropTypes<RawProps>,
Bindings = UnwrapBindings<RawBindings> Bindings = UnwrapValues<RawBindings>
>( >(
options: ComponentOptions<RawProps, RawBindings, Props, Bindings> options: ComponentOptions<RawProps, RawBindings, Props, Bindings>
): { ): {
@ -119,6 +120,7 @@ export function createComponentInstance(type: any): ComponentInstance {
rtg: null, rtg: null,
rtc: null, rtc: null,
ec: null, ec: null,
effects: null,
// public properties // public properties
state: EMPTY_OBJ, state: EMPTY_OBJ,

View File

@ -6,13 +6,8 @@ function injectHook(
target: ComponentInstance | null | void = currentInstance target: ComponentInstance | null | void = currentInstance
) { ) {
if (target) { if (target) {
const existing = target[name]
// TODO inject a error-handling wrapped version of the hook // TODO inject a error-handling wrapped version of the hook
if (existing !== null) { ;(target[name] || (target[name] = [])).push(hook)
existing.push(hook)
} else {
target[name] = [hook]
}
} else { } else {
// TODO warn // TODO warn
} }

View File

@ -769,17 +769,7 @@ export function createRenderer(options: RendererOptions) {
function unmount(vnode: VNode, doRemove?: boolean) { function unmount(vnode: VNode, doRemove?: boolean) {
const instance = vnode.component const instance = vnode.component
if (instance != null) { if (instance != null) {
// beforeUnmount hook unmountComponent(instance, doRemove)
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)
}
return return
} }
const shouldRemoveChildren = vnode.type === Fragment && doRemove 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( function unmountChildren(
children: VNode[], children: VNode[],
doRemove?: boolean, doRemove?: boolean,

View File

@ -19,4 +19,4 @@ export * from './componentLifecycle'
export { createRenderer, RendererOptions } from './createRenderer' export { createRenderer, RendererOptions } from './createRenderer'
export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' export { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
export * from '@vue/observer' export * from './reactivity'

View 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
}

View File

@ -78,7 +78,7 @@ export function createVNode(
type, type,
props, props,
key: props && props.key, key: props && props.key,
children, children: typeof children === 'number' ? children + '' : children,
component: null, component: null,
el: null, el: null,
anchor: null, anchor: null,