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_INITIAL_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_INITIAL_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,
|
observeEvenIfShallow = false
|
) {
|
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_INITIAL_VALUE || arguments.length === 2)
|
) {
|
val = obj[key]
|
}
|
|
let childOb = shallow ? val && val.__ob__ : 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 ? newVal && newVal.__ob__ : 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<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
|
}
|
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<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)
|
}
|
}
|
}
|