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
|
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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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'
|
||||||
|
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,
|
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,
|
||||||
|
Loading…
Reference in New Issue
Block a user