UNPKG

11.2 kBJavaScriptView Raw
1/* @flow */
2
3import config from '../config'
4import { warn } from './debug'
5import { set } from '../observer/index'
6import { unicodeLetters } from './lang'
7import { nativeWatch, hasSymbol } from './env'
8
9import {
10 ASSET_TYPES,
11 LIFECYCLE_HOOKS
12} from 'shared/constants'
13
14import {
15 extend,
16 hasOwn,
17 camelize,
18 toRawType,
19 capitalize,
20 isBuiltInTag,
21 isPlainObject
22} from 'shared/util'
23
24/**
25 * Option overwriting strategies are functions that handle
26 * how to merge a parent option value and a child option
27 * value into the final value.
28 */
29const strats = config.optionMergeStrategies
30
31/**
32 * Options with restrictions
33 */
34if (process.env.NODE_ENV !== 'production') {
35 strats.el = strats.propsData = function (parent, child, vm, key) {
36 if (!vm) {
37 warn(
38 `option "${key}" can only be used during instance ` +
39 'creation with the `new` keyword.'
40 )
41 }
42 return defaultStrat(parent, child)
43 }
44}
45
46/**
47 * Helper that recursively merges two data objects together.
48 */
49function mergeData (to: Object, from: ?Object): Object {
50 if (!from) return to
51 let key, toVal, fromVal
52
53 const keys = hasSymbol
54 ? Reflect.ownKeys(from)
55 : Object.keys(from)
56
57 for (let i = 0; i < keys.length; i++) {
58 key = keys[i]
59 // in case the object is already observed...
60 if (key === '__ob__') continue
61 toVal = to[key]
62 fromVal = from[key]
63 if (!hasOwn(to, key)) {
64 set(to, key, fromVal)
65 } else if (
66 toVal !== fromVal &&
67 isPlainObject(toVal) &&
68 isPlainObject(fromVal)
69 ) {
70 mergeData(toVal, fromVal)
71 }
72 }
73 return to
74}
75
76/**
77 * Data
78 */
79export function mergeDataOrFn (
80 parentVal: any,
81 childVal: any,
82 vm?: Component
83): ?Function {
84 if (!vm) {
85 // in a Vue.extend merge, both should be functions
86 if (!childVal) {
87 return parentVal
88 }
89 if (!parentVal) {
90 return childVal
91 }
92 // when parentVal & childVal are both present,
93 // we need to return a function that returns the
94 // merged result of both functions... no need to
95 // check if parentVal is a function here because
96 // it has to be a function to pass previous merges.
97 return function mergedDataFn () {
98 return mergeData(
99 typeof childVal === 'function' ? childVal.call(this, this) : childVal,
100 typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
101 )
102 }
103 } else {
104 return function mergedInstanceDataFn () {
105 // instance merge
106 const instanceData = typeof childVal === 'function'
107 ? childVal.call(vm, vm)
108 : childVal
109 const defaultData = typeof parentVal === 'function'
110 ? parentVal.call(vm, vm)
111 : parentVal
112 if (instanceData) {
113 return mergeData(instanceData, defaultData)
114 } else {
115 return defaultData
116 }
117 }
118 }
119}
120
121strats.data = function (
122 parentVal: any,
123 childVal: any,
124 vm?: Component
125): ?Function {
126 if (!vm) {
127 if (childVal && typeof childVal !== 'function') {
128 process.env.NODE_ENV !== 'production' && warn(
129 'The "data" option should be a function ' +
130 'that returns a per-instance value in component ' +
131 'definitions.',
132 vm
133 )
134
135 return parentVal
136 }
137 return mergeDataOrFn(parentVal, childVal)
138 }
139
140 return mergeDataOrFn(parentVal, childVal, vm)
141}
142
143/**
144 * Hooks and props are merged as arrays.
145 */
146function mergeHook (
147 parentVal: ?Array<Function>,
148 childVal: ?Function | ?Array<Function>
149): ?Array<Function> {
150 const res = childVal
151 ? parentVal
152 ? parentVal.concat(childVal)
153 : Array.isArray(childVal)
154 ? childVal
155 : [childVal]
156 : parentVal
157 return res
158 ? dedupeHooks(res)
159 : res
160}
161
162function dedupeHooks (hooks) {
163 const res = []
164 for (let i = 0; i < hooks.length; i++) {
165 if (res.indexOf(hooks[i]) === -1) {
166 res.push(hooks[i])
167 }
168 }
169 return res
170}
171
172LIFECYCLE_HOOKS.forEach(hook => {
173 strats[hook] = mergeHook
174})
175
176/**
177 * Assets
178 *
179 * When a vm is present (instance creation), we need to do
180 * a three-way merge between constructor options, instance
181 * options and parent options.
182 */
183function mergeAssets (
184 parentVal: ?Object,
185 childVal: ?Object,
186 vm?: Component,
187 key: string
188): Object {
189 const res = Object.create(parentVal || null)
190 if (childVal) {
191 process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
192 return extend(res, childVal)
193 } else {
194 return res
195 }
196}
197
198ASSET_TYPES.forEach(function (type) {
199 strats[type + 's'] = mergeAssets
200})
201
202/**
203 * Watchers.
204 *
205 * Watchers hashes should not overwrite one
206 * another, so we merge them as arrays.
207 */
208strats.watch = function (
209 parentVal: ?Object,
210 childVal: ?Object,
211 vm?: Component,
212 key: string
213): ?Object {
214 // work around Firefox's Object.prototype.watch...
215 if (parentVal === nativeWatch) parentVal = undefined
216 if (childVal === nativeWatch) childVal = undefined
217 /* istanbul ignore if */
218 if (!childVal) return Object.create(parentVal || null)
219 if (process.env.NODE_ENV !== 'production') {
220 assertObjectType(key, childVal, vm)
221 }
222 if (!parentVal) return childVal
223 const ret = {}
224 extend(ret, parentVal)
225 for (const key in childVal) {
226 let parent = ret[key]
227 const child = childVal[key]
228 if (parent && !Array.isArray(parent)) {
229 parent = [parent]
230 }
231 ret[key] = parent
232 ? parent.concat(child)
233 : Array.isArray(child) ? child : [child]
234 }
235 return ret
236}
237
238/**
239 * Other object hashes.
240 */
241strats.props =
242strats.methods =
243strats.inject =
244strats.computed = function (
245 parentVal: ?Object,
246 childVal: ?Object,
247 vm?: Component,
248 key: string
249): ?Object {
250 if (childVal && process.env.NODE_ENV !== 'production') {
251 assertObjectType(key, childVal, vm)
252 }
253 if (!parentVal) return childVal
254 const ret = Object.create(null)
255 extend(ret, parentVal)
256 if (childVal) extend(ret, childVal)
257 return ret
258}
259strats.provide = mergeDataOrFn
260
261/**
262 * Default strategy.
263 */
264const defaultStrat = function (parentVal: any, childVal: any): any {
265 return childVal === undefined
266 ? parentVal
267 : childVal
268}
269
270/**
271 * Validate component names
272 */
273function checkComponents (options: Object) {
274 for (const key in options.components) {
275 validateComponentName(key)
276 }
277}
278
279export function validateComponentName (name: string) {
280 if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeLetters}]*$`).test(name)) {
281 warn(
282 'Invalid component name: "' + name + '". Component names ' +
283 'should conform to valid custom element name in html5 specification.'
284 )
285 }
286 if (isBuiltInTag(name) || config.isReservedTag(name)) {
287 warn(
288 'Do not use built-in or reserved HTML elements as component ' +
289 'id: ' + name
290 )
291 }
292}
293
294/**
295 * Ensure all props option syntax are normalized into the
296 * Object-based format.
297 */
298function normalizeProps (options: Object, vm: ?Component) {
299 const props = options.props
300 if (!props) return
301 const res = {}
302 let i, val, name
303 if (Array.isArray(props)) {
304 i = props.length
305 while (i--) {
306 val = props[i]
307 if (typeof val === 'string') {
308 name = camelize(val)
309 res[name] = { type: null }
310 } else if (process.env.NODE_ENV !== 'production') {
311 warn('props must be strings when using array syntax.')
312 }
313 }
314 } else if (isPlainObject(props)) {
315 for (const key in props) {
316 val = props[key]
317 name = camelize(key)
318 res[name] = isPlainObject(val)
319 ? val
320 : { type: val }
321 }
322 } else if (process.env.NODE_ENV !== 'production') {
323 warn(
324 `Invalid value for option "props": expected an Array or an Object, ` +
325 `but got ${toRawType(props)}.`,
326 vm
327 )
328 }
329 options.props = res
330}
331
332/**
333 * Normalize all injections into Object-based format
334 */
335function normalizeInject (options: Object, vm: ?Component) {
336 const inject = options.inject
337 if (!inject) return
338 const normalized = options.inject = {}
339 if (Array.isArray(inject)) {
340 for (let i = 0; i < inject.length; i++) {
341 normalized[inject[i]] = { from: inject[i] }
342 }
343 } else if (isPlainObject(inject)) {
344 for (const key in inject) {
345 const val = inject[key]
346 normalized[key] = isPlainObject(val)
347 ? extend({ from: key }, val)
348 : { from: val }
349 }
350 } else if (process.env.NODE_ENV !== 'production') {
351 warn(
352 `Invalid value for option "inject": expected an Array or an Object, ` +
353 `but got ${toRawType(inject)}.`,
354 vm
355 )
356 }
357}
358
359/**
360 * Normalize raw function directives into object format.
361 */
362function normalizeDirectives (options: Object) {
363 const dirs = options.directives
364 if (dirs) {
365 for (const key in dirs) {
366 const def = dirs[key]
367 if (typeof def === 'function') {
368 dirs[key] = { bind: def, update: def }
369 }
370 }
371 }
372}
373
374function assertObjectType (name: string, value: any, vm: ?Component) {
375 if (!isPlainObject(value)) {
376 warn(
377 `Invalid value for option "${name}": expected an Object, ` +
378 `but got ${toRawType(value)}.`,
379 vm
380 )
381 }
382}
383
384/**
385 * Merge two option objects into a new one.
386 * Core utility used in both instantiation and inheritance.
387 */
388export function mergeOptions (
389 parent: Object,
390 child: Object,
391 vm?: Component
392): Object {
393 if (process.env.NODE_ENV !== 'production') {
394 checkComponents(child)
395 }
396
397 if (typeof child === 'function') {
398 child = child.options
399 }
400
401 normalizeProps(child, vm)
402 normalizeInject(child, vm)
403 normalizeDirectives(child)
404
405 // Apply extends and mixins on the child options,
406 // but only if it is a raw options object that isn't
407 // the result of another mergeOptions call.
408 // Only merged options has the _base property.
409 if (!child._base) {
410 if (child.extends) {
411 parent = mergeOptions(parent, child.extends, vm)
412 }
413 if (child.mixins) {
414 for (let i = 0, l = child.mixins.length; i < l; i++) {
415 parent = mergeOptions(parent, child.mixins[i], vm)
416 }
417 }
418 }
419
420 const options = {}
421 let key
422 for (key in parent) {
423 mergeField(key)
424 }
425 for (key in child) {
426 if (!hasOwn(parent, key)) {
427 mergeField(key)
428 }
429 }
430 function mergeField (key) {
431 const strat = strats[key] || defaultStrat
432 options[key] = strat(parent[key], child[key], vm, key)
433 }
434 return options
435}
436
437/**
438 * Resolve an asset.
439 * This function is used because child instances need access
440 * to assets defined in its ancestor chain.
441 */
442export function resolveAsset (
443 options: Object,
444 type: string,
445 id: string,
446 warnMissing?: boolean
447): any {
448 /* istanbul ignore if */
449 if (typeof id !== 'string') {
450 return
451 }
452 const assets = options[type]
453 // check local registration variations first
454 if (hasOwn(assets, id)) return assets[id]
455 const camelizedId = camelize(id)
456 if (hasOwn(assets, camelizedId)) return assets[camelizedId]
457 const PascalCaseId = capitalize(camelizedId)
458 if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
459 // fallback to prototype chain
460 const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
461 if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
462 warn(
463 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
464 options
465 )
466 }
467 return res
468}