diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index a40a3488..1790aa33 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -4,7 +4,8 @@ import { Data, RenderFunction, ComponentOptions, - ComponentPropsOptions + ComponentPropsOptions, + WatchOptions } from './componentOptions' import { setupWatcher } from './componentWatch' import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer' @@ -102,9 +103,10 @@ export class Component { $watch( this: MountedComponent, keyOrFn: string | (() => any), - cb: () => void + cb: () => void, + options?: WatchOptions ) { - return setupWatcher(this, keyOrFn, cb) + return setupWatcher(this, keyOrFn, cb, options) } // eventEmitter interface diff --git a/packages/core/src/componentOptions.ts b/packages/core/src/componentOptions.ts index 2bcbf1a3..d459a82e 100644 --- a/packages/core/src/componentOptions.ts +++ b/packages/core/src/componentOptions.ts @@ -43,9 +43,23 @@ export interface ComponentComputedOptions { } export interface ComponentWatchOptions { - [key: string]: ( - this: MountedComponent & D & P, - oldValue: any, - newValue: any - ) => void + [key: string]: ComponentWatchOption & D & P> +} + +export type ComponentWatchOption = + | WatchHandler + | WatchHandler[] + | WatchOptionsWithHandler + | string + +export type WatchHandler = (this: C, val: any, oldVal: any) => void + +export interface WatchOptionsWithHandler extends WatchOptions { + handler: WatchHandler +} + +export interface WatchOptions { + sync?: boolean + deep?: boolean + immediate?: boolean } diff --git a/packages/core/src/componentWatch.ts b/packages/core/src/componentWatch.ts index a847ed3f..17a9eb12 100644 --- a/packages/core/src/componentWatch.ts +++ b/packages/core/src/componentWatch.ts @@ -1,7 +1,9 @@ +import { EMPTY_OBJ } from './utils' import { MountedComponent } from './component' -import { ComponentWatchOptions } from './componentOptions' +import { ComponentWatchOptions, WatchOptions } from './componentOptions' import { autorun, stop } from '@vue/observer' import { queueJob } from '@vue/scheduler' +import { handleError, ErrorTypes } from './errorHandling' export function initializeWatch( instance: MountedComponent, @@ -9,17 +11,25 @@ export function initializeWatch( ) { if (options !== void 0) { for (const key in options) { - setupWatcher(instance, key, options[key]) + const opt = options[key] + if (Array.isArray(opt)) { + opt.forEach(o => setupWatcher(instance, key, o)) + } else if (typeof opt === 'function') { + setupWatcher(instance, key, opt) + } else if (typeof opt === 'string') { + setupWatcher(instance, key, (instance as any)[opt]) + } else if (opt.handler) { + setupWatcher(instance, key, opt.handler, opt) + } } } } -// TODO deep watch -// TODO sync watch export function setupWatcher( instance: MountedComponent, keyOrFn: string | Function, - cb: Function + cb: (newValue: any, oldValue: any) => void, + options: WatchOptions = EMPTY_OBJ as WatchOptions ): () => void { const handles = instance._watchHandles || (instance._watchHandles = new Set()) const proxy = instance.$proxy @@ -29,28 +39,40 @@ export function setupWatcher( ? () => proxy[keyOrFn] : () => keyOrFn.call(proxy) + const getter = options.deep ? () => traverse(rawGetter()) : rawGetter + let oldValue: any const applyCb = () => { const newValue = runner() - if (newValue !== oldValue) { - // TODO handle error - cb(newValue, oldValue) + if (options.deep || newValue !== oldValue) { oldValue = newValue + try { + cb.call(instance.$proxy, newValue, oldValue) + } catch (e) { + handleError(e, instance, ErrorTypes.WATCH_CALLBACK) + } } } - const runner = autorun(rawGetter, { - scheduler: () => { - // defer watch callback using the scheduler so that multiple mutations - // result in one call only. - queueJob(applyCb) - } + const runner = autorun(getter, { + lazy: true, + scheduler: options.sync + ? applyCb + : () => { + // defer watch callback using the scheduler so that multiple mutations + // result in one call only. + queueJob(applyCb) + } }) oldValue = runner() handles.add(runner) + if (options.immediate) { + cb.call(instance.$proxy, oldValue, undefined) + } + return () => { stop(runner) handles.delete(runner) @@ -62,3 +84,24 @@ export function teardownWatch(instance: MountedComponent) { instance._watchHandles.forEach(stop) } } + +function traverse(value: any, seen: Set = new Set()) { + if (value === null || typeof value !== 'object' || seen.has(value)) { + return + } + seen.add(value) + if (Array.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 +} diff --git a/packages/observer/src/index.ts b/packages/observer/src/index.ts index ca181a56..7f8a9f5a 100644 --- a/packages/observer/src/index.ts +++ b/packages/observer/src/index.ts @@ -83,8 +83,11 @@ function createObservable( baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler ) { - if ((__DEV__ && target === null) || typeof target !== 'object') { - throw new Error(`value is not observable: ${String(target)}`) + if (target === null || typeof target !== 'object') { + if (__DEV__) { + console.warn(`value is not observable: ${String(target)}`) + } + return target } // target already has corresponding Proxy let observed = toProxy.get(target)