feat: mixins/extends/assets options
This commit is contained in:
parent
0bdf205a73
commit
02de984f1f
@ -207,6 +207,4 @@ describe('api: createApp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test.todo('mixin')
|
test.todo('mixin')
|
||||||
|
|
||||||
test.todo('config.optionsMergeStrategies')
|
|
||||||
})
|
})
|
||||||
|
17
packages/runtime-core/__tests__/apiOptions.spec.ts
Normal file
17
packages/runtime-core/__tests__/apiOptions.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
describe('api: options', () => {
|
||||||
|
test('data', () => {})
|
||||||
|
|
||||||
|
test('computed', () => {})
|
||||||
|
|
||||||
|
test('methods', () => {})
|
||||||
|
|
||||||
|
test('watch', () => {})
|
||||||
|
|
||||||
|
test('provide/inject', () => {})
|
||||||
|
|
||||||
|
test('mixins', () => {})
|
||||||
|
|
||||||
|
test('extends', () => {})
|
||||||
|
|
||||||
|
test('lifecycle', () => {})
|
||||||
|
})
|
@ -3,14 +3,12 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ComponentRenderProxy,
|
ComponentRenderProxy,
|
||||||
Data,
|
Data,
|
||||||
ComponentInstance,
|
ComponentInstance
|
||||||
currentRenderingInstance,
|
|
||||||
currentInstance
|
|
||||||
} from './component'
|
} from './component'
|
||||||
import { Directive } from './directives'
|
import { Directive } from './directives'
|
||||||
import { HostNode, RootRenderFunction } from './createRenderer'
|
import { HostNode, RootRenderFunction } from './createRenderer'
|
||||||
import { InjectionKey } from './apiInject'
|
import { InjectionKey } from './apiInject'
|
||||||
import { isFunction, camelize, capitalize } from '@vue/shared'
|
import { isFunction } from '@vue/shared'
|
||||||
import { warn } from './warning'
|
import { warn } from './warning'
|
||||||
import { createVNode } from './vnode'
|
import { createVNode } from './vnode'
|
||||||
|
|
||||||
@ -164,35 +162,3 @@ export function createAppAPI(render: RootRenderFunction): () => App {
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveAsset(type: 'components' | 'directives', name: string) {
|
|
||||||
const instance = currentRenderingInstance || currentInstance
|
|
||||||
if (instance) {
|
|
||||||
let camelized
|
|
||||||
let capitalized
|
|
||||||
let res
|
|
||||||
const local = (instance.type as any)[type]
|
|
||||||
if (local) {
|
|
||||||
res =
|
|
||||||
local[name] ||
|
|
||||||
local[(camelized = camelize(name))] ||
|
|
||||||
local[(capitalized = capitalize(camelized))]
|
|
||||||
}
|
|
||||||
if (!res) {
|
|
||||||
const global = instance.appContext[type]
|
|
||||||
res =
|
|
||||||
global[name] ||
|
|
||||||
global[camelized || (camelized = camelize(name))] ||
|
|
||||||
global[capitalized || capitalize(camelized)]
|
|
||||||
}
|
|
||||||
if (__DEV__ && !res) {
|
|
||||||
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
} else if (__DEV__) {
|
|
||||||
warn(
|
|
||||||
`resolve${capitalize(type.slice(0, -1))} ` +
|
|
||||||
`can only be used in render() or setup().`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,7 +2,8 @@ import {
|
|||||||
ComponentInstance,
|
ComponentInstance,
|
||||||
Data,
|
Data,
|
||||||
ComponentOptions,
|
ComponentOptions,
|
||||||
ComponentRenderProxy
|
currentRenderingInstance,
|
||||||
|
currentInstance
|
||||||
} from './component'
|
} from './component'
|
||||||
import {
|
import {
|
||||||
isFunction,
|
isFunction,
|
||||||
@ -10,10 +11,12 @@ import {
|
|||||||
isString,
|
isString,
|
||||||
isObject,
|
isObject,
|
||||||
isArray,
|
isArray,
|
||||||
EMPTY_OBJ
|
EMPTY_OBJ,
|
||||||
|
capitalize,
|
||||||
|
camelize
|
||||||
} from '@vue/shared'
|
} from '@vue/shared'
|
||||||
import { computed, ComputedOptions } from './apiReactivity'
|
import { computed, ComputedOptions } from './apiReactivity'
|
||||||
import { watch, WatchOptions } from './apiWatch'
|
import { watch } from './apiWatch'
|
||||||
import { provide, inject } from './apiInject'
|
import { provide, inject } from './apiInject'
|
||||||
import {
|
import {
|
||||||
onBeforeMount,
|
onBeforeMount,
|
||||||
@ -26,13 +29,10 @@ import {
|
|||||||
onUnmounted
|
onUnmounted
|
||||||
} from './apiLifecycle'
|
} from './apiLifecycle'
|
||||||
import { DebuggerEvent } from '@vue/reactivity'
|
import { DebuggerEvent } from '@vue/reactivity'
|
||||||
|
import { warn } from './warning'
|
||||||
|
|
||||||
type LegacyComponent =
|
// TODO legacy component definition also supports constructors with .options
|
||||||
| ComponentOptions
|
type LegacyComponent = ComponentOptions
|
||||||
| {
|
|
||||||
new (): ComponentRenderProxy
|
|
||||||
options: ComponentOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO type inference for these options
|
// TODO type inference for these options
|
||||||
export interface LegacyOptions {
|
export interface LegacyOptions {
|
||||||
@ -77,17 +77,29 @@ export interface LegacyOptions {
|
|||||||
errorCaptured?(): boolean
|
errorCaptured?(): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processOptions(instance: ComponentInstance) {
|
export function applyOptions(
|
||||||
|
instance: ComponentInstance,
|
||||||
|
options: ComponentOptions,
|
||||||
|
asMixin: boolean = false
|
||||||
|
) {
|
||||||
const data =
|
const data =
|
||||||
instance.data === EMPTY_OBJ ? (instance.data = {}) : instance.data
|
instance.data === EMPTY_OBJ ? (instance.data = {}) : instance.data
|
||||||
const ctx = instance.renderProxy as any
|
const ctx = instance.renderProxy as any
|
||||||
const {
|
const {
|
||||||
|
// composition
|
||||||
|
mixins,
|
||||||
|
extends: extendsOptions,
|
||||||
|
// state
|
||||||
data: dataOptions,
|
data: dataOptions,
|
||||||
computed: computedOptions,
|
computed: computedOptions,
|
||||||
methods,
|
methods,
|
||||||
watch: watchOptions,
|
watch: watchOptions,
|
||||||
provide: provideOptions,
|
provide: provideOptions,
|
||||||
inject: injectOptions,
|
inject: injectOptions,
|
||||||
|
// assets
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
// lifecycle
|
||||||
// beforeCreate is handled separately
|
// beforeCreate is handled separately
|
||||||
created,
|
created,
|
||||||
beforeMount,
|
beforeMount,
|
||||||
@ -101,24 +113,36 @@ export function processOptions(instance: ComponentInstance) {
|
|||||||
renderTracked,
|
renderTracked,
|
||||||
renderTriggered,
|
renderTriggered,
|
||||||
errorCaptured
|
errorCaptured
|
||||||
} = instance.type as ComponentOptions
|
} = options
|
||||||
|
|
||||||
|
// global mixins are applied first, and only if this is a non-mixin call
|
||||||
|
// so that they are applied once per instance.
|
||||||
|
if (!asMixin) {
|
||||||
|
applyMixins(instance, instance.appContext.mixins)
|
||||||
|
}
|
||||||
|
// extending a base component...
|
||||||
|
if (extendsOptions) {
|
||||||
|
applyOptions(instance, extendsOptions, true)
|
||||||
|
}
|
||||||
|
// local mixins
|
||||||
|
if (mixins) {
|
||||||
|
applyMixins(instance, mixins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// state options
|
||||||
if (dataOptions) {
|
if (dataOptions) {
|
||||||
extend(data, isFunction(dataOptions) ? dataOptions.call(ctx) : dataOptions)
|
extend(data, isFunction(dataOptions) ? dataOptions.call(ctx) : dataOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computedOptions) {
|
if (computedOptions) {
|
||||||
for (const key in computedOptions) {
|
for (const key in computedOptions) {
|
||||||
data[key] = computed(computedOptions[key] as any)
|
data[key] = computed(computedOptions[key] as any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (methods) {
|
if (methods) {
|
||||||
for (const key in methods) {
|
for (const key in methods) {
|
||||||
data[key] = methods[key].bind(ctx)
|
data[key] = methods[key].bind(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (watchOptions) {
|
if (watchOptions) {
|
||||||
for (const key in watchOptions) {
|
for (const key in watchOptions) {
|
||||||
const raw = watchOptions[key]
|
const raw = watchOptions[key]
|
||||||
@ -140,7 +164,6 @@ export function processOptions(instance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provideOptions) {
|
if (provideOptions) {
|
||||||
const provides = isFunction(provideOptions)
|
const provides = isFunction(provideOptions)
|
||||||
? provideOptions.call(ctx)
|
? provideOptions.call(ctx)
|
||||||
@ -149,7 +172,6 @@ export function processOptions(instance: ComponentInstance) {
|
|||||||
provide(key, provides[key])
|
provide(key, provides[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (injectOptions) {
|
if (injectOptions) {
|
||||||
if (isArray(injectOptions)) {
|
if (isArray(injectOptions)) {
|
||||||
for (let i = 0; i < injectOptions.length; i++) {
|
for (let i = 0; i < injectOptions.length; i++) {
|
||||||
@ -168,6 +190,15 @@ export function processOptions(instance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// asset options
|
||||||
|
if (components) {
|
||||||
|
extend(instance.components, components)
|
||||||
|
}
|
||||||
|
if (directives) {
|
||||||
|
extend(instance.directives, directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lifecycle options
|
||||||
if (created) {
|
if (created) {
|
||||||
created.call(ctx)
|
created.call(ctx)
|
||||||
}
|
}
|
||||||
@ -200,15 +231,29 @@ export function processOptions(instance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function legacyWatch(
|
function applyMixins(instance: ComponentInstance, mixins: ComponentOptions[]) {
|
||||||
this: ComponentInstance,
|
for (let i = 0; i < mixins.length; i++) {
|
||||||
source: string | Function,
|
applyOptions(instance, mixins[i], true)
|
||||||
cb: Function,
|
}
|
||||||
options?: WatchOptions
|
}
|
||||||
): () => void {
|
|
||||||
const ctx = this.renderProxy as any
|
export function resolveAsset(type: 'components' | 'directives', name: string) {
|
||||||
const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
|
const instance = currentRenderingInstance || currentInstance
|
||||||
const stop = watch(getter, cb.bind(ctx), options)
|
if (instance) {
|
||||||
onBeforeMount(stop, this)
|
let camelized
|
||||||
return stop
|
const registry = instance[type]
|
||||||
|
const res =
|
||||||
|
registry[name] ||
|
||||||
|
registry[(camelized = camelize(name))] ||
|
||||||
|
registry[capitalize(camelized)]
|
||||||
|
if (__DEV__ && !res) {
|
||||||
|
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
} else if (__DEV__) {
|
||||||
|
warn(
|
||||||
|
`resolve${capitalize(type.slice(0, -1))} ` +
|
||||||
|
`can only be used in render() or setup().`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,15 @@ import {
|
|||||||
ReactiveEffectOptions
|
ReactiveEffectOptions
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import { queueJob, queuePostFlushCb } from './scheduler'
|
import { queueJob, queuePostFlushCb } from './scheduler'
|
||||||
import { EMPTY_OBJ, isObject, isArray, isFunction } from '@vue/shared'
|
import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared'
|
||||||
import { recordEffect } from './apiReactivity'
|
import { recordEffect } from './apiReactivity'
|
||||||
import { currentInstance } from './component'
|
import { currentInstance, ComponentInstance } from './component'
|
||||||
import {
|
import {
|
||||||
ErrorTypes,
|
ErrorTypes,
|
||||||
callWithErrorHandling,
|
callWithErrorHandling,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
} from './errorHandling'
|
} from './errorHandling'
|
||||||
|
import { onBeforeMount } from './apiLifecycle'
|
||||||
|
|
||||||
export interface WatchOptions {
|
export interface WatchOptions {
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
@ -187,6 +188,20 @@ function doWatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this.$watch
|
||||||
|
export function instanceWatch(
|
||||||
|
this: ComponentInstance,
|
||||||
|
source: string | Function,
|
||||||
|
cb: Function,
|
||||||
|
options?: WatchOptions
|
||||||
|
): () => void {
|
||||||
|
const ctx = this.renderProxy as any
|
||||||
|
const getter = isString(source) ? () => ctx[source] : source.bind(ctx)
|
||||||
|
const stop = watch(getter, cb.bind(ctx), options)
|
||||||
|
onBeforeMount(stop, this)
|
||||||
|
return stop
|
||||||
|
}
|
||||||
|
|
||||||
function traverse(value: any, seen: Set<any> = new Set()) {
|
function traverse(value: any, seen: Set<any> = new Set()) {
|
||||||
if (!isObject(value) || seen.has(value)) {
|
if (!isObject(value) || seen.has(value)) {
|
||||||
return
|
return
|
||||||
|
@ -20,9 +20,9 @@ import {
|
|||||||
callWithErrorHandling,
|
callWithErrorHandling,
|
||||||
callWithAsyncErrorHandling
|
callWithAsyncErrorHandling
|
||||||
} from './errorHandling'
|
} from './errorHandling'
|
||||||
import { AppContext, createAppContext, resolveAsset } from './apiApp'
|
import { AppContext, createAppContext } from './apiApp'
|
||||||
import { Directive } from './directives'
|
import { Directive } from './directives'
|
||||||
import { processOptions, LegacyOptions } from './apiOptions'
|
import { applyOptions, LegacyOptions, resolveAsset } from './apiOptions'
|
||||||
|
|
||||||
export type Data = { [key: string]: unknown }
|
export type Data = { [key: string]: unknown }
|
||||||
|
|
||||||
@ -129,6 +129,9 @@ export type ComponentInstance<P = Data, S = Data> = {
|
|||||||
effects: ReactiveEffect[] | null
|
effects: ReactiveEffect[] | null
|
||||||
provides: Data
|
provides: Data
|
||||||
|
|
||||||
|
components: Record<string, Component>
|
||||||
|
directives: Record<string, Directive>
|
||||||
|
|
||||||
// the rest are only for stateful components
|
// the rest are only for stateful components
|
||||||
data: S
|
data: S
|
||||||
props: P
|
props: P
|
||||||
@ -211,7 +214,7 @@ export function createComponentInstance(
|
|||||||
vnode,
|
vnode,
|
||||||
parent,
|
parent,
|
||||||
appContext,
|
appContext,
|
||||||
type: vnode.type as any,
|
type: vnode.type as Component,
|
||||||
root: null as any, // set later so it can point to itself
|
root: null as any, // set later so it can point to itself
|
||||||
next: null,
|
next: null,
|
||||||
subTree: null as any,
|
subTree: null as any,
|
||||||
@ -230,6 +233,10 @@ export function createComponentInstance(
|
|||||||
slots: EMPTY_OBJ,
|
slots: EMPTY_OBJ,
|
||||||
refs: EMPTY_OBJ,
|
refs: EMPTY_OBJ,
|
||||||
|
|
||||||
|
// per-instance asset storage (mutable during options resolution)
|
||||||
|
components: Object.create(appContext.components),
|
||||||
|
directives: Object.create(appContext.directives),
|
||||||
|
|
||||||
// user namespace for storing whatever the user assigns to `this`
|
// user namespace for storing whatever the user assigns to `this`
|
||||||
user: {},
|
user: {},
|
||||||
|
|
||||||
@ -351,7 +358,7 @@ export function setupStatefulComponent(instance: ComponentInstance) {
|
|||||||
}
|
}
|
||||||
// support for 2.x options
|
// support for 2.x options
|
||||||
if (__FEATURE_OPTIONS__) {
|
if (__FEATURE_OPTIONS__) {
|
||||||
processOptions(instance)
|
applyOptions(instance, Component)
|
||||||
}
|
}
|
||||||
instance.data = reactive(instance.data === EMPTY_OBJ ? {} : instance.data)
|
instance.data = reactive(instance.data === EMPTY_OBJ ? {} : instance.data)
|
||||||
currentInstance = null
|
currentInstance = null
|
||||||
@ -491,5 +498,5 @@ function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveComponent(name: string): Component | undefined {
|
export function resolveComponent(name: string): Component | undefined {
|
||||||
return resolveAsset('components', name)
|
return resolveAsset('components', name) as any
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ComponentInstance } from './component'
|
import { ComponentInstance } from './component'
|
||||||
import { nextTick } from './scheduler'
|
import { nextTick } from './scheduler'
|
||||||
import { legacyWatch } from './apiOptions'
|
import { instanceWatch } from './apiWatch'
|
||||||
|
|
||||||
export const RenderProxyHandlers = {
|
export const RenderProxyHandlers = {
|
||||||
get(target: ComponentInstance, key: string) {
|
get(target: ComponentInstance, key: string) {
|
||||||
@ -42,7 +42,7 @@ export const RenderProxyHandlers = {
|
|||||||
case '$nextTick':
|
case '$nextTick':
|
||||||
return nextTick
|
return nextTick
|
||||||
case '$watch':
|
case '$watch':
|
||||||
return legacyWatch.bind(target)
|
return instanceWatch.bind(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return target.user[key]
|
return target.user[key]
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from './component'
|
} from './component'
|
||||||
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
|
import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling'
|
||||||
import { HostNode } from './createRenderer'
|
import { HostNode } from './createRenderer'
|
||||||
import { resolveAsset } from './apiApp'
|
import { resolveAsset } from './apiOptions'
|
||||||
|
|
||||||
export interface DirectiveBinding {
|
export interface DirectiveBinding {
|
||||||
instance: ComponentRenderProxy | null
|
instance: ComponentRenderProxy | null
|
||||||
@ -138,5 +138,5 @@ export function invokeDirectiveHook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveDirective(name: string): Directive | undefined {
|
export function resolveDirective(name: string): Directive | undefined {
|
||||||
return resolveAsset('directives', name)
|
return resolveAsset('directives', name) as any
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user