feat: full watch api

This commit is contained in:
Evan You 2018-09-23 23:16:14 -04:00
parent cb01733842
commit ddd55fae54
4 changed files with 86 additions and 24 deletions

View File

@ -4,7 +4,8 @@ import {
Data, Data,
RenderFunction, RenderFunction,
ComponentOptions, ComponentOptions,
ComponentPropsOptions ComponentPropsOptions,
WatchOptions
} from './componentOptions' } from './componentOptions'
import { setupWatcher } from './componentWatch' import { setupWatcher } from './componentWatch'
import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer' import { Autorun, DebuggerEvent, ComputedGetter } from '@vue/observer'
@ -102,9 +103,10 @@ export class Component {
$watch( $watch(
this: MountedComponent, this: MountedComponent,
keyOrFn: string | (() => any), keyOrFn: string | (() => any),
cb: () => void cb: () => void,
options?: WatchOptions
) { ) {
return setupWatcher(this, keyOrFn, cb) return setupWatcher(this, keyOrFn, cb, options)
} }
// eventEmitter interface // eventEmitter interface

View File

@ -43,9 +43,23 @@ export interface ComponentComputedOptions<D = Data, P = Data> {
} }
export interface ComponentWatchOptions<D = Data, P = Data> { export interface ComponentWatchOptions<D = Data, P = Data> {
[key: string]: ( [key: string]: ComponentWatchOption<MountedComponent<D, P> & D & P>
this: MountedComponent<D, P> & D & P, }
oldValue: any,
newValue: any export type ComponentWatchOption<C = any> =
) => void | WatchHandler<C>
| WatchHandler<C>[]
| WatchOptionsWithHandler<C>
| string
export type WatchHandler<C = any> = (this: C, val: any, oldVal: any) => void
export interface WatchOptionsWithHandler<C = any> extends WatchOptions {
handler: WatchHandler<C>
}
export interface WatchOptions {
sync?: boolean
deep?: boolean
immediate?: boolean
} }

View File

@ -1,7 +1,9 @@
import { EMPTY_OBJ } from './utils'
import { MountedComponent } from './component' import { MountedComponent } from './component'
import { ComponentWatchOptions } from './componentOptions' import { ComponentWatchOptions, WatchOptions } from './componentOptions'
import { autorun, stop } from '@vue/observer' import { autorun, stop } from '@vue/observer'
import { queueJob } from '@vue/scheduler' import { queueJob } from '@vue/scheduler'
import { handleError, ErrorTypes } from './errorHandling'
export function initializeWatch( export function initializeWatch(
instance: MountedComponent, instance: MountedComponent,
@ -9,17 +11,25 @@ export function initializeWatch(
) { ) {
if (options !== void 0) { if (options !== void 0) {
for (const key in options) { 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( export function setupWatcher(
instance: MountedComponent, instance: MountedComponent,
keyOrFn: string | Function, keyOrFn: string | Function,
cb: Function cb: (newValue: any, oldValue: any) => void,
options: WatchOptions = EMPTY_OBJ as WatchOptions
): () => void { ): () => void {
const handles = instance._watchHandles || (instance._watchHandles = new Set()) const handles = instance._watchHandles || (instance._watchHandles = new Set())
const proxy = instance.$proxy const proxy = instance.$proxy
@ -29,28 +39,40 @@ export function setupWatcher(
? () => proxy[keyOrFn] ? () => proxy[keyOrFn]
: () => keyOrFn.call(proxy) : () => keyOrFn.call(proxy)
const getter = options.deep ? () => traverse(rawGetter()) : rawGetter
let oldValue: any let oldValue: any
const applyCb = () => { const applyCb = () => {
const newValue = runner() const newValue = runner()
if (newValue !== oldValue) { if (options.deep || newValue !== oldValue) {
// TODO handle error
cb(newValue, oldValue)
oldValue = newValue oldValue = newValue
try {
cb.call(instance.$proxy, newValue, oldValue)
} catch (e) {
handleError(e, instance, ErrorTypes.WATCH_CALLBACK)
}
} }
} }
const runner = autorun(rawGetter, { const runner = autorun(getter, {
scheduler: () => { lazy: true,
// defer watch callback using the scheduler so that multiple mutations scheduler: options.sync
// result in one call only. ? applyCb
queueJob(applyCb) : () => {
} // defer watch callback using the scheduler so that multiple mutations
// result in one call only.
queueJob(applyCb)
}
}) })
oldValue = runner() oldValue = runner()
handles.add(runner) handles.add(runner)
if (options.immediate) {
cb.call(instance.$proxy, oldValue, undefined)
}
return () => { return () => {
stop(runner) stop(runner)
handles.delete(runner) handles.delete(runner)
@ -62,3 +84,24 @@ export function teardownWatch(instance: MountedComponent) {
instance._watchHandles.forEach(stop) instance._watchHandles.forEach(stop)
} }
} }
function traverse(value: any, seen: Set<any> = 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
}

View File

@ -83,8 +83,11 @@ function createObservable(
baseHandlers: ProxyHandler<any>, baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any> collectionHandlers: ProxyHandler<any>
) { ) {
if ((__DEV__ && target === null) || typeof target !== 'object') { if (target === null || typeof target !== 'object') {
throw new Error(`value is not observable: ${String(target)}`) if (__DEV__) {
console.warn(`value is not observable: ${String(target)}`)
}
return target
} }
// target already has corresponding Proxy // target already has corresponding Proxy
let observed = toProxy.get(target) let observed = toProxy.get(target)