feat: 2.x options support

This commit is contained in:
Evan You 2019-09-03 22:25:38 -04:00
parent c833db9c97
commit a6616e4210
6 changed files with 243 additions and 14 deletions

View File

@ -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'],

View File

@ -7,7 +7,7 @@ export interface ComputedRef<T> {
readonly effect: ReactiveEffect
}
export interface ComputedOptions<T> {
export interface ComputedOptions<T = any> {
get: () => T
set: (v: T) => void
}

View File

@ -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 ` +

View File

@ -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<string, (() => any) | ComputedOptions>
methods?: Record<string, Function>
// 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))
}
}

View File

@ -17,7 +17,8 @@ export {
OperationTypes,
Ref,
ComputedRef,
UnwrapRef
UnwrapRef,
ComputedOptions
} from '@vue/reactivity'
import {

View File

@ -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<Props = {}, RawBindings = {}> = <
this: ComponentRenderProxy<Props, Bindings>
) => VNodeChild
interface ComponentOptionsBase<Props, RawBindings> {
interface ComponentOptionsBase<Props, RawBindings> extends LegacyOptions {
setup?: (
props: Props,
ctx: SetupContext
) => RawBindings | (() => VNodeChild) | void
name?: string
template?: string
render?: RenderFunction<Props, RawBindings>
components?: Record<string, Component>
directives?: Record<string, Directive>
// TODO full 2.x options compat
}
interface ComponentOptionsWithoutProps<Props = {}, RawBindings = {}>
@ -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