]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: mixins/extends/assets options
authorEvan You <yyx990803@gmail.com>
Wed, 4 Sep 2019 15:36:27 +0000 (11:36 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 4 Sep 2019 15:37:00 +0000 (11:37 -0400)
packages/runtime-core/__tests__/apiApp.spec.ts
packages/runtime-core/__tests__/apiOptions.spec.ts [new file with mode: 0644]
packages/runtime-core/src/apiApp.ts
packages/runtime-core/src/apiOptions.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/directives.ts

index 07e23113c66e12399f79b41337e4a1f3b52a10ef..fa91792e614bd69f69219ed9fe614a96ef44dfdd 100644 (file)
@@ -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 (file)
index 0000000..f63f50f
--- /dev/null
@@ -0,0 +1,17 @@
+describe('api: options', () => {
+  test('data', () => {})
+
+  test('computed', () => {})
+
+  test('methods', () => {})
+
+  test('watch', () => {})
+
+  test('provide/inject', () => {})
+
+  test('mixins', () => {})
+
+  test('extends', () => {})
+
+  test('lifecycle', () => {})
+})
index bb0455ab4d72fc7e38c9ac6936efbf17c4b885bd..cd410c4eb9e72dec302163f7bf535891b3b95864 100644 (file)
@@ -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().`
-    )
-  }
-}
index 6c87daccb45cade661354a17a68666c99ae3f970..bb95f4e2eb2448a04f2ad1531f723c5f2a2fa464 100644 (file)
@@ -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().`
+    )
+  }
 }
index 9ff82eaf2bdedecca7f514283ce4df5c1fb7f87c..2b85b3838db14aa2da515dcf30964d91f396d342 100644 (file)
@@ -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<any> = new Set()) {
   if (!isObject(value) || seen.has(value)) {
     return
index 1b6d55bbf0e14660b78eb7fc67c10e6470791af5..48c352397e7ceeaa38da6c749a8566dedc5f8af6 100644 (file)
@@ -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<P = Data, S = Data> = {
   effects: ReactiveEffect[] | null
   provides: Data
 
+  components: Record<string, Component>
+  directives: Record<string, Directive>
+
   // 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
 }
index 16d1a0e1c69a07d7ccbcb71ad15a76bf7dd043ca..891490d68bfa9498924042a7e196d2f9d15c71bb 100644 (file)
@@ -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]
index 1c901de8e1e83a6af316e4c02815fd603f0675dc..d18afeace50e4f8a50cfb1db3a0dc756486c1374 100644 (file)
@@ -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
 }