UNPKG

10.2 kBJavaScriptView Raw
1/* @flow */
2
3import config from '../config'
4import Watcher from '../observer/watcher'
5import Dep, { pushTarget, popTarget } from '../observer/dep'
6import { isUpdatingChildComponent } from './lifecycle'
7
8import {
9 set,
10 del,
11 observe,
12 defineReactive,
13 toggleObserving
14} from '../observer/index'
15
16import {
17 warn,
18 bind,
19 noop,
20 hasOwn,
21 hyphenate,
22 isReserved,
23 handleError,
24 nativeWatch,
25 validateProp,
26 isPlainObject,
27 isServerRendering,
28 isReservedAttribute
29} from '../util/index'
30
31const sharedPropertyDefinition = {
32 enumerable: true,
33 configurable: true,
34 get: noop,
35 set: noop
36}
37
38export function proxy (target: Object, sourceKey: string, key: string) {
39 sharedPropertyDefinition.get = function proxyGetter () {
40 return this[sourceKey][key]
41 }
42 sharedPropertyDefinition.set = function proxySetter (val) {
43 this[sourceKey][key] = val
44 }
45 Object.defineProperty(target, key, sharedPropertyDefinition)
46}
47
48export function initState (vm: Component) {
49 vm._watchers = []
50 const opts = vm.$options
51 if (opts.props) initProps(vm, opts.props)
52 if (opts.methods) initMethods(vm, opts.methods)
53 if (opts.data) {
54 initData(vm)
55 } else {
56 observe(vm._data = {}, true /* asRootData */)
57 }
58 if (opts.computed) initComputed(vm, opts.computed)
59 if (opts.watch && opts.watch !== nativeWatch) {
60 initWatch(vm, opts.watch)
61 }
62}
63
64function initProps (vm: Component, propsOptions: Object) {
65 const propsData = vm.$options.propsData || {}
66 const props = vm._props = {}
67 // cache prop keys so that future props updates can iterate using Array
68 // instead of dynamic object key enumeration.
69 const keys = vm.$options._propKeys = []
70 const isRoot = !vm.$parent
71 // root instance props should be converted
72 if (!isRoot) {
73 toggleObserving(false)
74 }
75 for (const key in propsOptions) {
76 keys.push(key)
77 const value = validateProp(key, propsOptions, propsData, vm)
78 /* istanbul ignore else */
79 if (process.env.NODE_ENV !== 'production') {
80 const hyphenatedKey = hyphenate(key)
81 if (isReservedAttribute(hyphenatedKey) ||
82 config.isReservedAttr(hyphenatedKey)) {
83 warn(
84 `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
85 vm
86 )
87 }
88 defineReactive(props, key, value, () => {
89 if (!isRoot && !isUpdatingChildComponent) {
90 warn(
91 `Avoid mutating a prop directly since the value will be ` +
92 `overwritten whenever the parent component re-renders. ` +
93 `Instead, use a data or computed property based on the prop's ` +
94 `value. Prop being mutated: "${key}"`,
95 vm
96 )
97 }
98 })
99 } else {
100 defineReactive(props, key, value)
101 }
102 // static props are already proxied on the component's prototype
103 // during Vue.extend(). We only need to proxy props defined at
104 // instantiation here.
105 if (!(key in vm)) {
106 proxy(vm, `_props`, key)
107 }
108 }
109 toggleObserving(true)
110}
111
112function initData (vm: Component) {
113 let data = vm.$options.data
114 data = vm._data = typeof data === 'function'
115 ? getData(data, vm)
116 : data || {}
117 if (!isPlainObject(data)) {
118 data = {}
119 process.env.NODE_ENV !== 'production' && warn(
120 'data functions should return an object:\n' +
121 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
122 vm
123 )
124 }
125 // proxy data on instance
126 const keys = Object.keys(data)
127 const props = vm.$options.props
128 const methods = vm.$options.methods
129 let i = keys.length
130 while (i--) {
131 const key = keys[i]
132 if (process.env.NODE_ENV !== 'production') {
133 if (methods && hasOwn(methods, key)) {
134 warn(
135 `Method "${key}" has already been defined as a data property.`,
136 vm
137 )
138 }
139 }
140 if (props && hasOwn(props, key)) {
141 process.env.NODE_ENV !== 'production' && warn(
142 `The data property "${key}" is already declared as a prop. ` +
143 `Use prop default value instead.`,
144 vm
145 )
146 } else if (!isReserved(key)) {
147 proxy(vm, `_data`, key)
148 }
149 }
150 // observe data
151 observe(data, true /* asRootData */)
152}
153
154export function getData (data: Function, vm: Component): any {
155 // #7573 disable dep collection when invoking data getters
156 pushTarget()
157 try {
158 return data.call(vm, vm)
159 } catch (e) {
160 handleError(e, vm, `data()`)
161 return {}
162 } finally {
163 popTarget()
164 }
165}
166
167const computedWatcherOptions = { lazy: true }
168
169function initComputed (vm: Component, computed: Object) {
170 // $flow-disable-line
171 const watchers = vm._computedWatchers = Object.create(null)
172 // computed properties are just getters during SSR
173 const isSSR = isServerRendering()
174
175 for (const key in computed) {
176 const userDef = computed[key]
177 const getter = typeof userDef === 'function' ? userDef : userDef.get
178 if (process.env.NODE_ENV !== 'production' && getter == null) {
179 warn(
180 `Getter is missing for computed property "${key}".`,
181 vm
182 )
183 }
184
185 if (!isSSR) {
186 // create internal watcher for the computed property.
187 watchers[key] = new Watcher(
188 vm,
189 getter || noop,
190 noop,
191 computedWatcherOptions
192 )
193 }
194
195 // component-defined computed properties are already defined on the
196 // component prototype. We only need to define computed properties defined
197 // at instantiation here.
198 if (!(key in vm)) {
199 defineComputed(vm, key, userDef)
200 } else if (process.env.NODE_ENV !== 'production') {
201 if (key in vm.$data) {
202 warn(`The computed property "${key}" is already defined in data.`, vm)
203 } else if (vm.$options.props && key in vm.$options.props) {
204 warn(`The computed property "${key}" is already defined as a prop.`, vm)
205 }
206 }
207 }
208}
209
210export function defineComputed (
211 target: any,
212 key: string,
213 userDef: Object | Function
214) {
215 const shouldCache = !isServerRendering()
216 if (typeof userDef === 'function') {
217 sharedPropertyDefinition.get = shouldCache
218 ? createComputedGetter(key)
219 : createGetterInvoker(userDef)
220 sharedPropertyDefinition.set = noop
221 } else {
222 sharedPropertyDefinition.get = userDef.get
223 ? shouldCache && userDef.cache !== false
224 ? createComputedGetter(key)
225 : createGetterInvoker(userDef.get)
226 : noop
227 sharedPropertyDefinition.set = userDef.set || noop
228 }
229 if (process.env.NODE_ENV !== 'production' &&
230 sharedPropertyDefinition.set === noop) {
231 sharedPropertyDefinition.set = function () {
232 warn(
233 `Computed property "${key}" was assigned to but it has no setter.`,
234 this
235 )
236 }
237 }
238 Object.defineProperty(target, key, sharedPropertyDefinition)
239}
240
241function createComputedGetter (key) {
242 return function computedGetter () {
243 const watcher = this._computedWatchers && this._computedWatchers[key]
244 if (watcher) {
245 if (watcher.dirty) {
246 watcher.evaluate()
247 }
248 if (Dep.target) {
249 watcher.depend()
250 }
251 return watcher.value
252 }
253 }
254}
255
256function createGetterInvoker(fn) {
257 return function computedGetter () {
258 return fn.call(this, this)
259 }
260}
261
262function initMethods (vm: Component, methods: Object) {
263 const props = vm.$options.props
264 for (const key in methods) {
265 if (process.env.NODE_ENV !== 'production') {
266 if (typeof methods[key] !== 'function') {
267 warn(
268 `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
269 `Did you reference the function correctly?`,
270 vm
271 )
272 }
273 if (props && hasOwn(props, key)) {
274 warn(
275 `Method "${key}" has already been defined as a prop.`,
276 vm
277 )
278 }
279 if ((key in vm) && isReserved(key)) {
280 warn(
281 `Method "${key}" conflicts with an existing Vue instance method. ` +
282 `Avoid defining component methods that start with _ or $.`
283 )
284 }
285 }
286 vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
287 }
288}
289
290function initWatch (vm: Component, watch: Object) {
291 for (const key in watch) {
292 const handler = watch[key]
293 if (Array.isArray(handler)) {
294 for (let i = 0; i < handler.length; i++) {
295 createWatcher(vm, key, handler[i])
296 }
297 } else {
298 createWatcher(vm, key, handler)
299 }
300 }
301}
302
303function createWatcher (
304 vm: Component,
305 expOrFn: string | Function,
306 handler: any,
307 options?: Object
308) {
309 if (isPlainObject(handler)) {
310 options = handler
311 handler = handler.handler
312 }
313 if (typeof handler === 'string') {
314 handler = vm[handler]
315 }
316 return vm.$watch(expOrFn, handler, options)
317}
318
319export function stateMixin (Vue: Class<Component>) {
320 // flow somehow has problems with directly declared definition object
321 // when using Object.defineProperty, so we have to procedurally build up
322 // the object here.
323 const dataDef = {}
324 dataDef.get = function () { return this._data }
325 const propsDef = {}
326 propsDef.get = function () { return this._props }
327 if (process.env.NODE_ENV !== 'production') {
328 dataDef.set = function () {
329 warn(
330 'Avoid replacing instance root $data. ' +
331 'Use nested data properties instead.',
332 this
333 )
334 }
335 propsDef.set = function () {
336 warn(`$props is readonly.`, this)
337 }
338 }
339 Object.defineProperty(Vue.prototype, '$data', dataDef)
340 Object.defineProperty(Vue.prototype, '$props', propsDef)
341
342 Vue.prototype.$set = set
343 Vue.prototype.$delete = del
344
345 Vue.prototype.$watch = function (
346 expOrFn: string | Function,
347 cb: any,
348 options?: Object
349 ): Function {
350 const vm: Component = this
351 if (isPlainObject(cb)) {
352 return createWatcher(vm, expOrFn, cb, options)
353 }
354 options = options || {}
355 options.user = true
356 const watcher = new Watcher(vm, expOrFn, cb, options)
357 if (options.immediate) {
358 try {
359 cb.call(vm, watcher.value)
360 } catch (error) {
361 handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
362 }
363 }
364 return function unwatchFn () {
365 watcher.teardown()
366 }
367 }
368}