UNPKG

6.86 kBJavaScriptView Raw
1/* @flow */
2
3import Dep from './dep'
4import VNode from '../vdom/vnode'
5import { arrayMethods } from './array'
6import {
7 def,
8 warn,
9 hasOwn,
10 hasProto,
11 isObject,
12 isPlainObject,
13 isPrimitive,
14 isUndef,
15 isValidArrayIndex,
16 isServerRendering
17} from '../util/index'
18
19const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
20
21/**
22 * In some cases we may want to disable observation inside a component's
23 * update computation.
24 */
25export let shouldObserve: boolean = true
26
27export function toggleObserving (value: boolean) {
28 shouldObserve = value
29}
30
31/**
32 * Observer class that is attached to each observed
33 * object. Once attached, the observer converts the target
34 * object's property keys into getter/setters that
35 * collect dependencies and dispatch updates.
36 */
37export class Observer {
38 value: any;
39 dep: Dep;
40 vmCount: number; // number of vms that have this object as root $data
41
42 constructor (value: any) {
43 this.value = value
44 this.dep = new Dep()
45 this.vmCount = 0
46 def(value, '__ob__', this)
47 if (Array.isArray(value)) {
48 if (hasProto) {
49 protoAugment(value, arrayMethods)
50 } else {
51 copyAugment(value, arrayMethods, arrayKeys)
52 }
53 this.observeArray(value)
54 } else {
55 this.walk(value)
56 }
57 }
58
59 /**
60 * Walk through all properties and convert them into
61 * getter/setters. This method should only be called when
62 * value type is Object.
63 */
64 walk (obj: Object) {
65 const keys = Object.keys(obj)
66 for (let i = 0; i < keys.length; i++) {
67 defineReactive(obj, keys[i])
68 }
69 }
70
71 /**
72 * Observe a list of Array items.
73 */
74 observeArray (items: Array<any>) {
75 for (let i = 0, l = items.length; i < l; i++) {
76 observe(items[i])
77 }
78 }
79}
80
81// helpers
82
83/**
84 * Augment a target Object or Array by intercepting
85 * the prototype chain using __proto__
86 */
87function protoAugment (target, src: Object) {
88 /* eslint-disable no-proto */
89 target.__proto__ = src
90 /* eslint-enable no-proto */
91}
92
93/**
94 * Augment a target Object or Array by defining
95 * hidden properties.
96 */
97/* istanbul ignore next */
98function copyAugment (target: Object, src: Object, keys: Array<string>) {
99 for (let i = 0, l = keys.length; i < l; i++) {
100 const key = keys[i]
101 def(target, key, src[key])
102 }
103}
104
105/**
106 * Attempt to create an observer instance for a value,
107 * returns the new observer if successfully observed,
108 * or the existing observer if the value already has one.
109 */
110export function observe (value: any, asRootData: ?boolean): Observer | void {
111 if (!isObject(value) || value instanceof VNode) {
112 return
113 }
114 let ob: Observer | void
115 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
116 ob = value.__ob__
117 } else if (
118 shouldObserve &&
119 !isServerRendering() &&
120 (Array.isArray(value) || isPlainObject(value)) &&
121 Object.isExtensible(value) &&
122 !value._isVue
123 ) {
124 ob = new Observer(value)
125 }
126 if (asRootData && ob) {
127 ob.vmCount++
128 }
129 return ob
130}
131
132/**
133 * Define a reactive property on an Object.
134 */
135export function defineReactive (
136 obj: Object,
137 key: string,
138 val: any,
139 customSetter?: ?Function,
140 shallow?: boolean
141) {
142 const dep = new Dep()
143
144 const property = Object.getOwnPropertyDescriptor(obj, key)
145 if (property && property.configurable === false) {
146 return
147 }
148
149 // cater for pre-defined getter/setters
150 const getter = property && property.get
151 const setter = property && property.set
152 if ((!getter || setter) && arguments.length === 2) {
153 val = obj[key]
154 }
155
156 let childOb = !shallow && observe(val)
157 Object.defineProperty(obj, key, {
158 enumerable: true,
159 configurable: true,
160 get: function reactiveGetter () {
161 const value = getter ? getter.call(obj) : val
162 if (Dep.target) {
163 dep.depend()
164 if (childOb) {
165 childOb.dep.depend()
166 if (Array.isArray(value)) {
167 dependArray(value)
168 }
169 }
170 }
171 return value
172 },
173 set: function reactiveSetter (newVal) {
174 const value = getter ? getter.call(obj) : val
175 /* eslint-disable no-self-compare */
176 if (newVal === value || (newVal !== newVal && value !== value)) {
177 return
178 }
179 /* eslint-enable no-self-compare */
180 if (process.env.NODE_ENV !== 'production' && customSetter) {
181 customSetter()
182 }
183 // #7981: for accessor properties without setter
184 if (getter && !setter) return
185 if (setter) {
186 setter.call(obj, newVal)
187 } else {
188 val = newVal
189 }
190 childOb = !shallow && observe(newVal)
191 dep.notify()
192 }
193 })
194}
195
196/**
197 * Set a property on an object. Adds the new property and
198 * triggers change notification if the property doesn't
199 * already exist.
200 */
201export function set (target: Array<any> | Object, key: any, val: any): any {
202 if (process.env.NODE_ENV !== 'production' &&
203 (isUndef(target) || isPrimitive(target))
204 ) {
205 warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
206 }
207 if (Array.isArray(target) && isValidArrayIndex(key)) {
208 target.length = Math.max(target.length, key)
209 target.splice(key, 1, val)
210 return val
211 }
212 if (key in target && !(key in Object.prototype)) {
213 target[key] = val
214 return val
215 }
216 const ob = (target: any).__ob__
217 if (target._isVue || (ob && ob.vmCount)) {
218 process.env.NODE_ENV !== 'production' && warn(
219 'Avoid adding reactive properties to a Vue instance or its root $data ' +
220 'at runtime - declare it upfront in the data option.'
221 )
222 return val
223 }
224 if (!ob) {
225 target[key] = val
226 return val
227 }
228 defineReactive(ob.value, key, val)
229 ob.dep.notify()
230 return val
231}
232
233/**
234 * Delete a property and trigger change if necessary.
235 */
236export function del (target: Array<any> | Object, key: any) {
237 if (process.env.NODE_ENV !== 'production' &&
238 (isUndef(target) || isPrimitive(target))
239 ) {
240 warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
241 }
242 if (Array.isArray(target) && isValidArrayIndex(key)) {
243 target.splice(key, 1)
244 return
245 }
246 const ob = (target: any).__ob__
247 if (target._isVue || (ob && ob.vmCount)) {
248 process.env.NODE_ENV !== 'production' && warn(
249 'Avoid deleting properties on a Vue instance or its root $data ' +
250 '- just set it to null.'
251 )
252 return
253 }
254 if (!hasOwn(target, key)) {
255 return
256 }
257 delete target[key]
258 if (!ob) {
259 return
260 }
261 ob.dep.notify()
262}
263
264/**
265 * Collect dependencies on array elements when the array is touched, since
266 * we cannot intercept array element access like property getters.
267 */
268function dependArray (value: Array<any>) {
269 for (let e, i = 0, l = value.length; i < l; i++) {
270 e = value[i]
271 e && e.__ob__ && e.__ob__.dep.depend()
272 if (Array.isArray(e)) {
273 dependArray(e)
274 }
275 }
276}