feat: full watch api
This commit is contained in:
parent
cb01733842
commit
ddd55fae54
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user