wip: watch
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user