diff --git a/jest.config.js b/jest.config.js index 08d09f94f..ab41aafa3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,8 +2,9 @@ module.exports = { preset: 'ts-jest', globals: { __DEV__: true, - __COMPAT__: false, - __JSDOM__: true + __JSDOM__: true, + __FEATURE_OPTIONS__: true, + __FEATURE_PRODUCTION_TIP__: false }, coverageDirectory: 'coverage', coverageReporters: ['html', 'lcov', 'text'], diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 5504ac79b..d79556440 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -7,7 +7,7 @@ export interface ComputedRef { readonly effect: ReactiveEffect } -export interface ComputedOptions { +export interface ComputedOptions { get: () => T set: (v: T) => void } diff --git a/packages/runtime-core/src/apiLifecycle.ts b/packages/runtime-core/src/apiLifecycle.ts index cded357b6..60470d828 100644 --- a/packages/runtime-core/src/apiLifecycle.ts +++ b/packages/runtime-core/src/apiLifecycle.ts @@ -12,7 +12,7 @@ import { capitalize } from '@vue/shared' function injectHook( type: LifecycleHooks, hook: Function, - target: ComponentInstance | null = currentInstance + target: ComponentInstance | null ) { if (target) { ;(target[type] || (target[type] = [])).push((...args: any[]) => { @@ -26,7 +26,7 @@ function injectHook( }) } else if (__DEV__) { const apiName = `on${capitalize( - ErrorTypeStrings[name].replace(/ hook$/, '') + ErrorTypeStrings[type].replace(/ hook$/, '') )}` warn( `${apiName} is called when there is no active component instance to be ` + diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/apiOptions.ts new file mode 100644 index 000000000..2ab2af29f --- /dev/null +++ b/packages/runtime-core/src/apiOptions.ts @@ -0,0 +1,203 @@ +import { + ComponentInstance, + Data, + ComponentOptions, + ComponentRenderProxy +} from './component' +import { + isFunction, + extend, + isString, + isObject, + isArray, + EMPTY_OBJ +} from '@vue/shared' +import { computed, ComputedOptions } from './apiReactivity' +import { watch } from './apiWatch' +import { provide, inject } from './apiInject' +import { + onBeforeMount, + onMounted, + onBeforeUpdate, + onUpdated, + onErrorCaptured, + onRenderTracked, + onBeforeUnmount, + onUnmounted +} from './apiLifecycle' +import { DebuggerEvent } from '@vue/reactivity' + +type LegacyComponent = + | ComponentOptions + | { + new (): ComponentRenderProxy + options: ComponentOptions + } + +// TODO type inference for these options +export interface LegacyOptions { + el?: any + + // state + data?: Data | (() => Data) + computed?: Record any) | ComputedOptions> + methods?: Record + // TODO watch array + watch?: Record< + string, + | string + | Function + | { handler: Function; deep?: boolean; immediate: boolean } + > + provide?: Data | (() => Data) + inject?: + | string[] + | Record< + string | symbol, + string | symbol | { from: string | symbol; default: any } + > + + // composition + mixins?: LegacyComponent[] + extends?: LegacyComponent + + // lifecycle + beforeCreate?(): void + created?(): void + beforeMount?(): void + mounted?(): void + beforeUpdate?(): void + updated?(): void + activated?(): void + decativated?(): void + beforeDestroy?(): void + destroyed?(): void + renderTracked?(e: DebuggerEvent): void + renderTriggered?(e: DebuggerEvent): void + errorCaptured?(): boolean +} + +export function processOptions(instance: ComponentInstance) { + const data = + instance.data === EMPTY_OBJ ? (instance.data = {}) : instance.data + const ctx = instance.renderProxy as any + const { + data: dataOptions, + computed: computedOptions, + methods, + watch: watchOptions, + provide: provideOptions, + inject: injectOptions, + // beforeCreate is handled separately + created, + beforeMount, + mounted, + beforeUpdate, + updated, + // TODO activated + // TODO decativated + beforeDestroy, + destroyed, + renderTracked, + renderTriggered, + errorCaptured + } = instance.type as ComponentOptions + + 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] + const getter = () => ctx[key] + if (isString(raw)) { + const handler = data[key] + if (isFunction(handler)) { + watch(getter, handler.bind(ctx)) + } else if (__DEV__) { + // TODO warn invalid watch handler path + } + } else if (isFunction(raw)) { + watch(getter, raw.bind(ctx)) + } else if (isObject(raw)) { + watch(getter, raw.handler.bind(ctx), { + deep: !!raw.deep, + lazy: !raw.immediate + }) + } else if (__DEV__) { + // TODO warn invalid watch options + } + } + } + + if (provideOptions) { + const provides = isFunction(provideOptions) + ? provideOptions.call(ctx) + : provideOptions + for (const key in provides) { + provide(key, provides[key]) + } + } + + if (injectOptions) { + if (isArray(injectOptions)) { + for (let i = 0; i < injectOptions.length; i++) { + const key = injectOptions[i] + data[key] = inject(key) + } + } else { + for (const key in injectOptions) { + const opt = injectOptions[key] + if (isObject(opt)) { + data[key] = inject(opt.from, opt.default) + } else { + data[key] = inject(opt) + } + } + } + } + + if (created) { + created.call(ctx) + } + if (beforeMount) { + onBeforeMount(beforeMount.bind(ctx)) + } + if (mounted) { + onMounted(mounted.bind(ctx)) + } + if (beforeUpdate) { + onBeforeUpdate(beforeUpdate.bind(ctx)) + } + if (updated) { + onUpdated(updated.bind(ctx)) + } + if (errorCaptured) { + onErrorCaptured(errorCaptured.bind(ctx)) + } + if (renderTracked) { + onRenderTracked(renderTracked.bind(ctx)) + } + if (renderTriggered) { + onRenderTracked(renderTriggered.bind(ctx)) + } + if (beforeDestroy) { + onBeforeUnmount(beforeDestroy.bind(ctx)) + } + if (destroyed) { + onUnmounted(destroyed.bind(ctx)) + } +} diff --git a/packages/runtime-core/src/apiReactivity.ts b/packages/runtime-core/src/apiReactivity.ts index 76bcf283c..40757ae49 100644 --- a/packages/runtime-core/src/apiReactivity.ts +++ b/packages/runtime-core/src/apiReactivity.ts @@ -17,7 +17,8 @@ export { OperationTypes, Ref, ComputedRef, - UnwrapRef + UnwrapRef, + ComputedOptions } from '@vue/reactivity' import { diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 8d26f4f03..1b6d55bbf 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1,6 +1,13 @@ import { VNode, normalizeVNode, VNodeChild, createVNode, Empty } from './vnode' import { ReactiveEffect, UnwrapRef, reactive, readonly } from '@vue/reactivity' -import { EMPTY_OBJ, isFunction, capitalize, NOOP, isArray } from '@vue/shared' +import { + EMPTY_OBJ, + isFunction, + capitalize, + NOOP, + isArray, + isObject +} from '@vue/shared' import { RenderProxyHandlers } from './componentProxy' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' import { Slots } from './componentSlots' @@ -15,6 +22,7 @@ import { } from './errorHandling' import { AppContext, createAppContext, resolveAsset } from './apiApp' import { Directive } from './directives' +import { processOptions, LegacyOptions } from './apiOptions' export type Data = { [key: string]: unknown } @@ -38,15 +46,16 @@ type RenderFunction = < this: ComponentRenderProxy ) => VNodeChild -interface ComponentOptionsBase { +interface ComponentOptionsBase extends LegacyOptions { setup?: ( props: Props, ctx: SetupContext ) => RawBindings | (() => VNodeChild) | void + name?: string + template?: string render?: RenderFunction components?: Record directives?: Record - // TODO full 2.x options compat } interface ComponentOptionsWithoutProps @@ -279,6 +288,7 @@ export const setCurrentInstance = (instance: ComponentInstance | null) => { } export function setupStatefulComponent(instance: ComponentInstance) { + currentInstance = instance const Component = instance.type as ComponentOptions // 1. create render proxy instance.renderProxy = new Proxy(instance, RenderProxyHandlers) as any @@ -291,15 +301,12 @@ export function setupStatefulComponent(instance: ComponentInstance) { if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null) - - currentInstance = instance const setupResult = callWithErrorHandling( setup, instance, ErrorTypes.SETUP_FUNCTION, [propsProxy, setupContext] ) - currentInstance = null if (isFunction(setupResult)) { // setup returned an inline render function @@ -322,15 +329,32 @@ export function setupStatefulComponent(instance: ComponentInstance) { } // setup returned bindings. // assuming a render function compiled from template is present. - instance.data = reactive(setupResult || {}) + if (isObject(setupResult)) { + instance.data = setupResult + } else if (__DEV__ && setupResult !== undefined) { + warn( + `setup() should return an object. Received: ${ + setupResult === null ? 'null' : typeof setupResult + }` + ) + } instance.render = (Component.render || NOOP) as RenderFunction } } else { if (__DEV__ && !Component.render) { - // TODO warn missing render fn + warn( + `Component is missing render function. Either provide a template or ` + + `return a render function from setup().` + ) } instance.render = Component.render as RenderFunction } + // support for 2.x options + if (__FEATURE_OPTIONS__) { + processOptions(instance) + } + instance.data = reactive(instance.data === EMPTY_OBJ ? {} : instance.data) + currentInstance = null } // used to identify a setup context proxy