feat: mixins/extends/assets options

This commit is contained in:
Evan You 2019-09-04 11:36:27 -04:00
parent 0bdf205a73
commit 02de984f1f
8 changed files with 124 additions and 76 deletions

View File

@ -207,6 +207,4 @@ describe('api: createApp', () => {
}) })
test.todo('mixin') test.todo('mixin')
test.todo('config.optionsMergeStrategies')
}) })

View 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', () => {})
})

View File

@ -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().`
)
}
}

View File

@ -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().`
)
}
} }

View File

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

View File

@ -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
} }

View File

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

View File

@ -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
} }