|
| 1 | +import { |
| 2 | + EMPTY_OBJ, |
| 3 | + isObject, |
| 4 | + isArray, |
| 5 | + isFunction, |
| 6 | + hasChanged, |
| 7 | + NOOP, |
| 8 | + isMap, |
| 9 | + isSet, |
| 10 | + isPlainObject, |
| 11 | + isPromise |
| 12 | +} from '@vue/shared' |
| 13 | +import { warn } from './warning' |
| 14 | +import { ComputedRef } from './computed' |
| 15 | +import { ReactiveFlags } from './constants' |
| 16 | +import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect' |
| 17 | +import { isShallow, isReactive } from './reactive' |
| 18 | +import { Ref, isRef } from './ref' |
| 19 | + |
| 20 | +// contexts where user provided function may be executed, in addition to |
| 21 | +// lifecycle hooks. |
| 22 | +export enum BaseWatchErrorCodes { |
| 23 | + WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER', |
| 24 | + WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK', |
| 25 | + WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP' |
| 26 | +} |
| 27 | + |
| 28 | +export interface SchedulerJob extends Function { |
| 29 | + id?: number |
| 30 | + pre?: boolean |
| 31 | + active?: boolean |
| 32 | + computed?: boolean |
| 33 | + /** |
| 34 | + * Indicates whether the effect is allowed to recursively trigger itself |
| 35 | + * when managed by the scheduler. |
| 36 | + * |
| 37 | + * By default, a job cannot trigger itself because some built-in method calls, |
| 38 | + * e.g. Array.prototype.push actually performs reads as well (#1740) which |
| 39 | + * can lead to confusing infinite loops. |
| 40 | + * The allowed cases are component update functions and watch callbacks. |
| 41 | + * Component update functions may update child component props, which in turn |
| 42 | + * trigger flush: "pre" watch callbacks that mutates state that the parent |
| 43 | + * relies on (#1801). Watch callbacks doesn't track its dependencies so if it |
| 44 | + * triggers itself again, it's likely intentional and it is the user's |
| 45 | + * responsibility to perform recursive state mutation that eventually |
| 46 | + * stabilizes (#1727). |
| 47 | + */ |
| 48 | + allowRecurse?: boolean |
| 49 | +} |
| 50 | + |
| 51 | +export type WatchEffect = (onCleanup: OnCleanup) => void |
| 52 | + |
| 53 | +export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) |
| 54 | + |
| 55 | +export type WatchCallback<V = any, OV = any> = ( |
| 56 | + value: V, |
| 57 | + oldValue: OV, |
| 58 | + onCleanup: OnCleanup |
| 59 | +) => any |
| 60 | + |
| 61 | +type OnCleanup = (cleanupFn: () => void) => void |
| 62 | + |
| 63 | +export interface BaseWatchOptions<Immediate = boolean> extends DebuggerOptions { |
| 64 | + immediate?: Immediate |
| 65 | + deep?: boolean |
| 66 | + once?: boolean |
| 67 | + scheduler?: Scheduler |
| 68 | + handlerError?: HandleError |
| 69 | + handlerWarn?: HandleWarn |
| 70 | +} |
| 71 | + |
| 72 | +export type WatchStopHandle = () => void |
| 73 | + |
| 74 | +// initial value for watchers to trigger on undefined initial values |
| 75 | +const INITIAL_WATCHER_VALUE = {} |
| 76 | + |
| 77 | +export type Scheduler = (context: { |
| 78 | + effect: ReactiveEffect |
| 79 | + job: SchedulerJob |
| 80 | + isInit: boolean |
| 81 | +}) => void |
| 82 | + |
| 83 | +const DEFAULT_SCHEDULER: Scheduler = ({ job }) => job() |
| 84 | + |
| 85 | +export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void |
| 86 | + |
| 87 | +const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => { |
| 88 | + throw err |
| 89 | +} |
| 90 | + |
| 91 | +export type HandleWarn = (msg: string, ...args: any[]) => void |
| 92 | + |
| 93 | +const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap() |
| 94 | +let activeEffect: ReactiveEffect | undefined = undefined |
| 95 | + |
| 96 | +export function onEffectCleanup(cleanupFn: () => void) { |
| 97 | + if (activeEffect) { |
| 98 | + const cleanups = |
| 99 | + cleanupMap.get(activeEffect) || |
| 100 | + cleanupMap.set(activeEffect, []).get(activeEffect)! |
| 101 | + cleanups.push(cleanupFn) |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +export function baseWatch( |
| 106 | + source: WatchSource | WatchSource[] | WatchEffect | object, |
| 107 | + cb: WatchCallback | null, |
| 108 | + { |
| 109 | + immediate, |
| 110 | + deep, |
| 111 | + once, |
| 112 | + onTrack, |
| 113 | + onTrigger, |
| 114 | + scheduler = DEFAULT_SCHEDULER, |
| 115 | + handlerError = DEFAULT_HANDLE_ERROR, |
| 116 | + handlerWarn = warn |
| 117 | + }: BaseWatchOptions = EMPTY_OBJ |
| 118 | +): WatchStopHandle { |
| 119 | + if (cb && once) { |
| 120 | + const _cb = cb |
| 121 | + cb = (...args) => { |
| 122 | + _cb(...args) |
| 123 | + unwatch() |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + const warnInvalidSource = (s: unknown) => { |
| 128 | + handlerWarn( |
| 129 | + `Invalid watch source: `, |
| 130 | + s, |
| 131 | + `A watch source can only be a getter/effect function, a ref, ` + |
| 132 | + `a reactive object, or an array of these types.` |
| 133 | + ) |
| 134 | + } |
| 135 | + |
| 136 | + let getter: () => any |
| 137 | + let forceTrigger = false |
| 138 | + let isMultiSource = false |
| 139 | + |
| 140 | + if (isRef(source)) { |
| 141 | + getter = () => source.value |
| 142 | + forceTrigger = isShallow(source) |
| 143 | + } else if (isReactive(source)) { |
| 144 | + getter = () => source |
| 145 | + deep = true |
| 146 | + } else if (isArray(source)) { |
| 147 | + isMultiSource = true |
| 148 | + forceTrigger = source.some(s => isReactive(s) || isShallow(s)) |
| 149 | + getter = () => |
| 150 | + source.map(s => { |
| 151 | + if (isRef(s)) { |
| 152 | + return s.value |
| 153 | + } else if (isReactive(s)) { |
| 154 | + return traverse(s) |
| 155 | + } else if (isFunction(s)) { |
| 156 | + return callWithErrorHandling( |
| 157 | + s, |
| 158 | + handlerError, |
| 159 | + BaseWatchErrorCodes.WATCH_GETTER |
| 160 | + ) |
| 161 | + } else { |
| 162 | + __DEV__ && warnInvalidSource(s) |
| 163 | + } |
| 164 | + }) |
| 165 | + } else if (isFunction(source)) { |
| 166 | + if (cb) { |
| 167 | + // getter with cb |
| 168 | + getter = () => |
| 169 | + callWithErrorHandling( |
| 170 | + source, |
| 171 | + handlerError, |
| 172 | + BaseWatchErrorCodes.WATCH_GETTER |
| 173 | + ) |
| 174 | + } else { |
| 175 | + // no cb -> simple effect |
| 176 | + getter = () => { |
| 177 | + // TODO: move to scheduler |
| 178 | + // if (instance && instance.isUnmounted) { |
| 179 | + // return |
| 180 | + // } |
| 181 | + if (cleanup) { |
| 182 | + cleanup() |
| 183 | + } |
| 184 | + const currentEffect = activeEffect |
| 185 | + activeEffect = effect |
| 186 | + try { |
| 187 | + return callWithAsyncErrorHandling( |
| 188 | + source, |
| 189 | + handlerError, |
| 190 | + BaseWatchErrorCodes.WATCH_CALLBACK, |
| 191 | + [onEffectCleanup] |
| 192 | + ) |
| 193 | + } finally { |
| 194 | + activeEffect = currentEffect |
| 195 | + } |
| 196 | + } |
| 197 | + } |
| 198 | + } else { |
| 199 | + getter = NOOP |
| 200 | + __DEV__ && warnInvalidSource(source) |
| 201 | + } |
| 202 | + |
| 203 | + if (cb && deep) { |
| 204 | + const baseGetter = getter |
| 205 | + getter = () => traverse(baseGetter()) |
| 206 | + } |
| 207 | + |
| 208 | + // TODO: support SSR |
| 209 | + // in SSR there is no need to setup an actual effect, and it should be noop |
| 210 | + // unless it's eager or sync flush |
| 211 | + // let ssrCleanup: (() => void)[] | undefined |
| 212 | + // if (__SSR__ && isInSSRComponentSetup) { |
| 213 | + // // we will also not call the invalidate callback (+ runner is not set up) |
| 214 | + // onCleanup = NOOP |
| 215 | + // if (!cb) { |
| 216 | + // getter() |
| 217 | + // } else if (immediate) { |
| 218 | + // callWithAsyncErrorHandling(cb, handlerError, BaseWatchErrorCodes.WATCH_CALLBACK, [ |
| 219 | + // getter(), |
| 220 | + // isMultiSource ? [] : undefined, |
| 221 | + // onCleanup |
| 222 | + // ]) |
| 223 | + // } |
| 224 | + // if (flush === 'sync') { |
| 225 | + // const ctx = useSSRContext()! |
| 226 | + // ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) |
| 227 | + // } else { |
| 228 | + // return NOOP |
| 229 | + // } |
| 230 | + // } |
| 231 | + |
| 232 | + let oldValue: any = isMultiSource |
| 233 | + ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) |
| 234 | + : INITIAL_WATCHER_VALUE |
| 235 | + const job: SchedulerJob = () => { |
| 236 | + if (!effect.active || !effect.dirty) { |
| 237 | + return |
| 238 | + } |
| 239 | + if (cb) { |
| 240 | + // watch(source, cb) |
| 241 | + const newValue = effect.run() |
| 242 | + if ( |
| 243 | + deep || |
| 244 | + forceTrigger || |
| 245 | + (isMultiSource |
| 246 | + ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) |
| 247 | + : hasChanged(newValue, oldValue)) |
| 248 | + ) { |
| 249 | + // cleanup before running cb again |
| 250 | + if (cleanup) { |
| 251 | + cleanup() |
| 252 | + } |
| 253 | + const currentEffect = activeEffect |
| 254 | + activeEffect = effect |
| 255 | + try { |
| 256 | + callWithAsyncErrorHandling( |
| 257 | + cb, |
| 258 | + handlerError, |
| 259 | + BaseWatchErrorCodes.WATCH_CALLBACK, |
| 260 | + [ |
| 261 | + newValue, |
| 262 | + // pass undefined as the old value when it's changed for the first time |
| 263 | + oldValue === INITIAL_WATCHER_VALUE |
| 264 | + ? undefined |
| 265 | + : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE |
| 266 | + ? [] |
| 267 | + : oldValue, |
| 268 | + onEffectCleanup |
| 269 | + ] |
| 270 | + ) |
| 271 | + oldValue = newValue |
| 272 | + } finally { |
| 273 | + activeEffect = currentEffect |
| 274 | + } |
| 275 | + } |
| 276 | + } else { |
| 277 | + // watchEffect |
| 278 | + effect.run() |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + // important: mark the job as a watcher callback so that scheduler knows |
| 283 | + // it is allowed to self-trigger (#1727) |
| 284 | + job.allowRecurse = !!cb |
| 285 | + |
| 286 | + let effectScheduler: EffectScheduler = () => |
| 287 | + scheduler({ |
| 288 | + effect, |
| 289 | + job, |
| 290 | + isInit: false |
| 291 | + }) |
| 292 | + |
| 293 | + const effect = new ReactiveEffect(getter, NOOP, effectScheduler) |
| 294 | + |
| 295 | + const cleanup = (effect.onStop = () => { |
| 296 | + const cleanups = cleanupMap.get(effect) |
| 297 | + if (cleanups) { |
| 298 | + cleanups.forEach(cleanup => cleanup()) |
| 299 | + cleanupMap.delete(effect) |
| 300 | + } |
| 301 | + }) |
| 302 | + |
| 303 | + const unwatch = () => { |
| 304 | + effect.stop() |
| 305 | + // TODO: move to doWatch |
| 306 | + // if (instance && instance.scope) { |
| 307 | + // remove(instance.scope.effects!, effect) |
| 308 | + // } |
| 309 | + } |
| 310 | + |
| 311 | + if (__DEV__) { |
| 312 | + effect.onTrack = onTrack |
| 313 | + effect.onTrigger = onTrigger |
| 314 | + } |
| 315 | + |
| 316 | + // initial run |
| 317 | + if (cb) { |
| 318 | + if (immediate) { |
| 319 | + job() |
| 320 | + } else { |
| 321 | + oldValue = effect.run() |
| 322 | + } |
| 323 | + } else { |
| 324 | + scheduler({ |
| 325 | + effect, |
| 326 | + job, |
| 327 | + isInit: true |
| 328 | + }) |
| 329 | + } |
| 330 | + |
| 331 | + return unwatch |
| 332 | +} |
| 333 | + |
| 334 | +export function traverse(value: unknown, seen?: Set<unknown>) { |
| 335 | + if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { |
| 336 | + return value |
| 337 | + } |
| 338 | + seen = seen || new Set() |
| 339 | + if (seen.has(value)) { |
| 340 | + return value |
| 341 | + } |
| 342 | + seen.add(value) |
| 343 | + if (isRef(value)) { |
| 344 | + traverse(value.value, seen) |
| 345 | + } else if (isArray(value)) { |
| 346 | + for (let i = 0; i < value.length; i++) { |
| 347 | + traverse(value[i], seen) |
| 348 | + } |
| 349 | + } else if (isSet(value) || isMap(value)) { |
| 350 | + value.forEach((v: any) => { |
| 351 | + traverse(v, seen) |
| 352 | + }) |
| 353 | + } else if (isPlainObject(value)) { |
| 354 | + for (const key in value) { |
| 355 | + traverse(value[key], seen) |
| 356 | + } |
| 357 | + } |
| 358 | + return value |
| 359 | +} |
| 360 | + |
| 361 | +export function callWithErrorHandling( |
| 362 | + fn: Function, |
| 363 | + handleError: HandleError, |
| 364 | + type: BaseWatchErrorCodes, |
| 365 | + args?: unknown[] |
| 366 | +) { |
| 367 | + let res |
| 368 | + try { |
| 369 | + res = args ? fn(...args) : fn() |
| 370 | + } catch (err) { |
| 371 | + handleError(err, type) |
| 372 | + } |
| 373 | + return res |
| 374 | +} |
| 375 | + |
| 376 | +export function callWithAsyncErrorHandling( |
| 377 | + fn: Function | Function[], |
| 378 | + handleError: HandleError, |
| 379 | + type: BaseWatchErrorCodes, |
| 380 | + args?: unknown[] |
| 381 | +): any[] { |
| 382 | + if (isFunction(fn)) { |
| 383 | + const res = callWithErrorHandling(fn, handleError, type, args) |
| 384 | + if (res && isPromise(res)) { |
| 385 | + res.catch(err => { |
| 386 | + handleError(err, type) |
| 387 | + }) |
| 388 | + } |
| 389 | + return res |
| 390 | + } |
| 391 | + |
| 392 | + const values = [] |
| 393 | + for (let i = 0; i < fn.length; i++) { |
| 394 | + values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args)) |
| 395 | + } |
| 396 | + return values |
| 397 | +} |
0 commit comments