import Dep from './dep' import VNode from '../vdom/vnode' import { arrayMethods } from './array' import { def, warn, hasOwn, isArray, hasProto, isPlainObject, isPrimitive, isUndef, isValidArrayIndex, isServerRendering, hasChanged, noop } from '../util/index' import { isReadonly, isRef, TrackOpTypes, TriggerOpTypes } from '../../v3' const arrayKeys = Object.getOwnPropertyNames(arrayMethods) const NO_INIITIAL_VALUE = {} /** * In some cases we may want to disable observation inside a component's * update computation. */ export let shouldObserve: boolean = true export function toggleObserving(value: boolean) { shouldObserve = value } // ssr mock dep const mockDep = { notify: noop, depend: noop, addSub: noop, removeSub: noop } as Dep /** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ export class Observer { dep: Dep vmCount: number // number of vms that have this object as root $data constructor(public value: any, public shallow = false, public mock = false) { // this.value = value this.dep = mock ? mockDep : new Dep() this.vmCount = 0 def(value, '__ob__', this) if (isArray(value)) { if (!mock) { if (hasProto) { /* eslint-disable no-proto */ ;(value as any).__proto__ = arrayMethods /* eslint-enable no-proto */ } else { for (let i = 0, l = arrayKeys.length; i < l; i++) { const key = arrayKeys[i] def(value, key, arrayMethods[key]) } } } if (!shallow) { this.observeArray(value) } } else { /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ const keys = Object.keys(value) for (let i = 0; i < keys.length; i++) { const key = keys[i] defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock) } } } /** * Observe a list of Array items. */ observeArray(value: any[]) { for (let i = 0, l = value.length; i < l; i++) { observe(value[i], false, this.mock) } } } // helpers /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe( value: any, shallow?: boolean, ssrMockReactivity?: boolean ): Observer | void { if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { return value.__ob__ } if ( shouldObserve && (ssrMockReactivity || !isServerRendering()) && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value.__v_skip /* ReactiveFlags.SKIP */ && !isRef(value) && !(value instanceof VNode) ) { return new Observer(value, shallow, ssrMockReactivity) } } /** * Define a reactive property on an Object. */ export function defineReactive( obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: boolean, mock?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ( (!getter || setter) && (val === NO_INIITIAL_VALUE || arguments.length === 2) ) { val = obj[key] } let childOb = !shallow && observe(val, false, mock) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if (Dep.target) { if (__DEV__) { dep.depend({ target: obj, type: TrackOpTypes.GET, key }) } else { dep.depend() } if (childOb) { childOb.dep.depend() if (isArray(value)) { dependArray(value) } } } return isRef(value) && !shallow ? value.value : value }, set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (!hasChanged(value, newVal)) { return } if (__DEV__ && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else if (getter) { // #7981: for accessor properties without setter return } else if (!shallow && isRef(value) && !isRef(newVal)) { value.value = newVal return } else { val = newVal } childOb = !shallow && observe(newVal, false, mock) if (__DEV__) { dep.notify({ type: TriggerOpTypes.SET, target: obj, key, newValue: newVal, oldValue: value }) } else { dep.notify() } } }) return dep } /** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set(array: T[], key: number, value: T): T export function set(object: object, key: string | number, value: T): T export function set( target: any[] | Record, key: any, val: any ): any { if (__DEV__ && (isUndef(target) || isPrimitive(target))) { warn( `Cannot set reactive property on undefined, null, or primitive value: ${target}` ) } if (isReadonly(target)) { __DEV__ && warn(`Set operation on key "${key}" failed: target is readonly.`) return } const ob = (target as any).__ob__ if (isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) // when mocking for SSR, array methods are not hijacked if (ob && !ob.shallow && ob.mock) { observe(val, false, true) } return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } if ((target as any)._isVue || (ob && ob.vmCount)) { __DEV__ && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock) if (__DEV__) { ob.dep.notify({ type: TriggerOpTypes.ADD, target: target, key, newValue: val, oldValue: undefined }) } else { ob.dep.notify() } return val } /** * Delete a property and trigger change if necessary. */ export function del(array: T[], key: number): void export function del(object: object, key: string | number): void export function del(target: any[] | object, key: any) { if (__DEV__ && (isUndef(target) || isPrimitive(target))) { warn( `Cannot delete reactive property on undefined, null, or primitive value: ${target}` ) } if (isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target as any).__ob__ if ((target as any)._isVue || (ob && ob.vmCount)) { __DEV__ && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } if (isReadonly(target)) { __DEV__ && warn(`Delete operation on key "${key}" failed: target is readonly.`) return } if (!hasOwn(target, key)) { return } delete target[key] if (!ob) { return } if (__DEV__) { ob.dep.notify({ type: TriggerOpTypes.DELETE, target: target, key }) } else { ob.dep.notify() } } /** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray(value: Array) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] if (e && e.__ob__) { e.__ob__.dep.depend() } if (isArray(e)) { dependArray(e) } } }