UNPKG

vue

Version:

Reactive, component-oriented view layer for modern web interfaces.

343 lines (322 loc) 8.24 kB
import Dep from './dep' import VNode from '../vdom/vnode' import { arrayMethods } from './array' import { def, warn, hasOwn, isArray, hasProto, isObject, isPlainObject, isPrimitive, isUndef, isValidArrayIndex, isServerRendering, hasChanged } 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 } /** * 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) { // this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } if (!shallow) { this.observeArray(value) } } else { this.walk(value, shallow) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk(obj: object, shallow: boolean) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { const key = keys[i] defineReactive(obj, key, NO_INIITIAL_VALUE, undefined, shallow) } } /** * Observe a list of Array items. */ observeArray(items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } // helpers /** * Augment a target Object or Array by intercepting * the prototype chain using __proto__ */ function protoAugment(target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } /** * Augment a target Object or Array by defining * hidden properties. */ /* istanbul ignore next */ function copyAugment(target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } } /** * 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): Observer | void { if (!isObject(value) || isRef(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value.__v_skip ) { ob = new Observer(value, shallow) } return ob } /** * Define a reactive property on an Object. */ export function defineReactive( obj: object, key: string, val?: any, customSetter?: Function | null, shallow?: 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) 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 (isRef(value) && !isRef(newVal)) { value.value = newVal return } else { val = newVal } childOb = !shallow && observe(newVal) 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<T>(array: T[], key: number, value: T): T export function set<T>(object: object, key: string | number, value: T): T export function set( target: any[] | Record<string, any>, 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 } if (isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target as any).__ob__ 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) 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<T>(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<any>) { 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) } } }