diff --git a/packages/runtime-core/__tests__/apiApp.spec.ts b/packages/runtime-core/__tests__/apiApp.spec.ts index 07e23113c..fa91792e6 100644 --- a/packages/runtime-core/__tests__/apiApp.spec.ts +++ b/packages/runtime-core/__tests__/apiApp.spec.ts @@ -207,6 +207,4 @@ describe('api: createApp', () => { }) test.todo('mixin') - - test.todo('config.optionsMergeStrategies') }) diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts new file mode 100644 index 000000000..f63f50f7b --- /dev/null +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -0,0 +1,17 @@ +describe('api: options', () => { + test('data', () => {}) + + test('computed', () => {}) + + test('methods', () => {}) + + test('watch', () => {}) + + test('provide/inject', () => {}) + + test('mixins', () => {}) + + test('extends', () => {}) + + test('lifecycle', () => {}) +}) diff --git a/packages/runtime-core/src/apiApp.ts b/packages/runtime-core/src/apiApp.ts index bb0455ab4..cd410c4eb 100644 --- a/packages/runtime-core/src/apiApp.ts +++ b/packages/runtime-core/src/apiApp.ts @@ -3,14 +3,12 @@ import { Component, ComponentRenderProxy, Data, - ComponentInstance, - currentRenderingInstance, - currentInstance + ComponentInstance } from './component' import { Directive } from './directives' import { HostNode, RootRenderFunction } from './createRenderer' import { InjectionKey } from './apiInject' -import { isFunction, camelize, capitalize } from '@vue/shared' +import { isFunction } from '@vue/shared' import { warn } from './warning' import { createVNode } from './vnode' @@ -164,35 +162,3 @@ export function createAppAPI(render: RootRenderFunction): () => 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().` - ) - } -} diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts index 6c87daccb..bb95f4e2e 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/apiOptions.ts @@ -2,7 +2,8 @@ import { ComponentInstance, Data, ComponentOptions, - ComponentRenderProxy + currentRenderingInstance, + currentInstance } from './component' import { isFunction, @@ -10,10 +11,12 @@ import { isString, isObject, isArray, - EMPTY_OBJ + EMPTY_OBJ, + capitalize, + camelize } from '@vue/shared' import { computed, ComputedOptions } from './apiReactivity' -import { watch, WatchOptions } from './apiWatch' +import { watch } from './apiWatch' import { provide, inject } from './apiInject' import { onBeforeMount, @@ -26,13 +29,10 @@ import { onUnmounted } from './apiLifecycle' import { DebuggerEvent } from '@vue/reactivity' +import { warn } from './warning' -type LegacyComponent = - | ComponentOptions - | { - new (): ComponentRenderProxy - options: ComponentOptions - } +// TODO legacy component definition also supports constructors with .options +type LegacyComponent = ComponentOptions // TODO type inference for these options export interface LegacyOptions { @@ -77,17 +77,29 @@ export interface LegacyOptions { errorCaptured?(): boolean } -export function processOptions(instance: ComponentInstance) { +export function applyOptions( + instance: ComponentInstance, + options: ComponentOptions, + asMixin: boolean = false +) { const data = instance.data === EMPTY_OBJ ? (instance.data = {}) : instance.data const ctx = instance.renderProxy as any const { + // composition + mixins, + extends: extendsOptions, + // state data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions, + // assets + components, + directives, + // lifecycle // beforeCreate is handled separately created, beforeMount, @@ -101,24 +113,36 @@ export function processOptions(instance: ComponentInstance) { renderTracked, renderTriggered, 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) { extend(data, isFunction(dataOptions) ? dataOptions.call(ctx) : dataOptions) } - if (computedOptions) { for (const key in computedOptions) { data[key] = computed(computedOptions[key] as any) } } - if (methods) { for (const key in methods) { data[key] = methods[key].bind(ctx) } } - if (watchOptions) { for (const key in watchOptions) { const raw = watchOptions[key] @@ -140,7 +164,6 @@ export function processOptions(instance: ComponentInstance) { } } } - if (provideOptions) { const provides = isFunction(provideOptions) ? provideOptions.call(ctx) @@ -149,7 +172,6 @@ export function processOptions(instance: ComponentInstance) { provide(key, provides[key]) } } - if (injectOptions) { if (isArray(injectOptions)) { 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) { created.call(ctx) } @@ -200,15 +231,29 @@ export function processOptions(instance: ComponentInstance) { } } -export function legacyWatch( - 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 applyMixins(instance: ComponentInstance, mixins: ComponentOptions[]) { + for (let i = 0; i < mixins.length; i++) { + applyOptions(instance, mixins[i], true) + } +} + +export function resolveAsset(type: 'components' | 'directives', name: string) { + const instance = currentRenderingInstance || currentInstance + if (instance) { + let camelized + 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().` + ) + } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 9ff82eaf2..2b85b3838 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -6,14 +6,15 @@ import { ReactiveEffectOptions } from '@vue/reactivity' 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 { currentInstance } from './component' +import { currentInstance, ComponentInstance } from './component' import { ErrorTypes, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' +import { onBeforeMount } from './apiLifecycle' export interface WatchOptions { 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 = new Set()) { if (!isObject(value) || seen.has(value)) { return diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 1b6d55bbf..48c352397 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -20,9 +20,9 @@ import { callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' -import { AppContext, createAppContext, resolveAsset } from './apiApp' +import { AppContext, createAppContext } from './apiApp' import { Directive } from './directives' -import { processOptions, LegacyOptions } from './apiOptions' +import { applyOptions, LegacyOptions, resolveAsset } from './apiOptions' export type Data = { [key: string]: unknown } @@ -129,6 +129,9 @@ export type ComponentInstance

= { effects: ReactiveEffect[] | null provides: Data + components: Record + directives: Record + // the rest are only for stateful components data: S props: P @@ -211,7 +214,7 @@ export function createComponentInstance( vnode, parent, appContext, - type: vnode.type as any, + type: vnode.type as Component, root: null as any, // set later so it can point to itself next: null, subTree: null as any, @@ -230,6 +233,10 @@ export function createComponentInstance( slots: 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: {}, @@ -351,7 +358,7 @@ export function setupStatefulComponent(instance: ComponentInstance) { } // support for 2.x options if (__FEATURE_OPTIONS__) { - processOptions(instance) + applyOptions(instance, Component) } instance.data = reactive(instance.data === EMPTY_OBJ ? {} : instance.data) currentInstance = null @@ -491,5 +498,5 @@ function hasPropsChanged(prevProps: Data, nextProps: Data): boolean { } export function resolveComponent(name: string): Component | undefined { - return resolveAsset('components', name) + return resolveAsset('components', name) as any } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 16d1a0e1c..891490d68 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -1,6 +1,6 @@ import { ComponentInstance } from './component' import { nextTick } from './scheduler' -import { legacyWatch } from './apiOptions' +import { instanceWatch } from './apiWatch' export const RenderProxyHandlers = { get(target: ComponentInstance, key: string) { @@ -42,7 +42,7 @@ export const RenderProxyHandlers = { case '$nextTick': return nextTick case '$watch': - return legacyWatch.bind(target) + return instanceWatch.bind(target) } } return target.user[key] diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index 1c901de8e..d18afeace 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -21,7 +21,7 @@ import { } from './component' import { callWithAsyncErrorHandling, ErrorTypes } from './errorHandling' import { HostNode } from './createRenderer' -import { resolveAsset } from './apiApp' +import { resolveAsset } from './apiOptions' export interface DirectiveBinding { instance: ComponentRenderProxy | null @@ -138,5 +138,5 @@ export function invokeDirectiveHook( } export function resolveDirective(name: string): Directive | undefined { - return resolveAsset('directives', name) + return resolveAsset('directives', name) as any }