]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: watch
authorEvan You <yyx990803@gmail.com>
Wed, 29 May 2019 15:44:59 +0000 (23:44 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 29 May 2019 15:44:59 +0000 (23:44 +0800)
packages/observer/src/computed.ts
packages/observer/src/effect.ts
packages/observer/src/index.ts
packages/observer/src/value.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentLifecycle.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/reactivity.ts [new file with mode: 0644]
packages/runtime-core/src/vnode.ts

index 5a517f780b523e34ee5ee053a9744e91d3c3e076..f91bfb472c777499ee11ec6fb13511629ea17a84 100644 (file)
@@ -15,12 +15,12 @@ export function computed<T, C = null>(
   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,
index e149de048c02d9a995f3405695140bbe1be1e2bf..37de5815c1ed785cd16f4e48b0415ef03277cbb1 100644 (file)
@@ -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
 }
index f3aea4e6d001368e3a97064cc6e0a16ed88f3522..44669a79821ae028bc13a5d7eefbe536c6f73f41 100644 (file)
@@ -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<any> = 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 = <T>(target?: T) => UnwrapBindings<T>
+type ObservableFactory = <T>(target?: T) => UnwrapValues<T>
 
 export const observable = ((target: any = {}): any => {
   // if trying to observe an immutable proxy, return the immutable version.
index c274eb0262b5d43b956e5fe78875ab2b03bfff5d..4101bbd35bf20866a90d48127be5dea46ccc67e8 100644 (file)
@@ -9,12 +9,34 @@ export interface 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
 
 // 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<T> = {
+export type UnwrapValues<T> = {
   [key in keyof T]: UnwrapValue<
     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)
-}
index 99416c562447f311f15e2e2ea33f56a9b5950850..e2db5c1101634721518991b8264c6d9a00144655 100644 (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,
index 2b0ed9d1d036754366bfa3a2ab6f9f72a178690a..a0525e177126e5339bdfb2a41f5eb3b5a5736c90 100644 (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
   }
index ba8ff88a294adeb6f22c3cf227264a5099342097..60505a428771a15be76b8e84b7c5e2a94a830403 100644 (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,
index 9a40d995afca904314ed91ef2c55405b87c090ba..2069e2f9bb0f9449080db92083e5fdb702b75dfe 100644 (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'
diff --git a/packages/runtime-core/src/reactivity.ts b/packages/runtime-core/src/reactivity.ts
new file mode 100644 (file)
index 0000000..493ae81
--- /dev/null
@@ -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
+}
index 6432b98116749b951c336effa1e2e736d95eb370..2b06f224801522cb59d4db084910a3881f25f261 100644 (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,