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

@@ -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,

View File

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

View File

@@ -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,

View File

@@ -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'

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,
props,
key: props && props.key,
children,
children: typeof children === 'number' ? children + '' : children,
component: null,
el: null,
anchor: null,