From 1e6e3a95e610913b0da8c6f5d50363096c0b20fd Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 21 Aug 2021 16:38:28 +0800 Subject: [PATCH] feat: implement api `useSlots` and `useAttrs` (#800) --- src/apis/index.ts | 1 + src/apis/setupHelpers.ts | 18 +++++++ src/component/componentOptions.ts | 29 +----------- src/component/index.ts | 1 - src/index.ts | 6 ++- src/mixin.ts | 33 +++++++------ src/runtimeContext.ts | 39 +++++++++++++++- src/utils/helper.ts | 7 +-- src/utils/instance.ts | 13 +++--- test/v3/runtime-core/apiSetupHelpers.spec.ts | 49 ++++++++++++++++++++ 10 files changed, 142 insertions(+), 54 deletions(-) create mode 100644 src/apis/setupHelpers.ts create mode 100644 test/v3/runtime-core/apiSetupHelpers.spec.ts diff --git a/src/apis/index.ts b/src/apis/index.ts index 598dbff6..515837da 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -25,3 +25,4 @@ export { getCurrentScope, onScopeDispose, } from './effectScope' +export { useAttrs, useSlots } from './setupHelpers' diff --git a/src/apis/setupHelpers.ts b/src/apis/setupHelpers.ts new file mode 100644 index 00000000..4b958221 --- /dev/null +++ b/src/apis/setupHelpers.ts @@ -0,0 +1,18 @@ +import { getCurrentInstance, SetupContext } from '../runtimeContext' +import { warn } from '../utils' + +export function useSlots(): SetupContext['slots'] { + return getContext().slots +} + +export function useAttrs(): SetupContext['attrs'] { + return getContext().attrs +} + +function getContext(): SetupContext { + const i = getCurrentInstance()! + if (__DEV__ && !i) { + warn(`useContext() called without active instance.`) + } + return i.setupContext! +} diff --git a/src/component/componentOptions.ts b/src/component/componentOptions.ts index eb8f7edb..117cd511 100644 --- a/src/component/componentOptions.ts +++ b/src/component/componentOptions.ts @@ -1,34 +1,9 @@ import Vue, { VNode, ComponentOptions as Vue2ComponentOptions } from 'vue' +import { SetupContext } from '../runtimeContext' import { Data } from './common' import { ComponentPropsOptions, ExtractPropTypes } from './componentProps' -import { ComponentInstance, ComponentRenderProxy } from './componentProxy' +import { ComponentRenderProxy } from './componentProxy' export { ComponentPropsOptions } from './componentProps' -export interface SetupContext { - readonly attrs: Data - readonly slots: Readonly<{ [key in string]?: (...args: any[]) => VNode[] }> - - /** - * @deprecated not available in Vue 3 - */ - readonly parent: ComponentInstance | null - - /** - * @deprecated not available in Vue 3 - */ - readonly root: ComponentInstance - - /** - * @deprecated not available in Vue 3 - */ - readonly listeners: { [key in string]?: Function } - - /** - * @deprecated not available in Vue 3 - */ - readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] } - - emit(event: string, ...args: any[]): void -} export type ComputedGetter = (ctx?: any) => T export type ComputedSetter = (v: T) => void diff --git a/src/component/index.ts b/src/component/index.ts index 86484560..c23a36fd 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -2,7 +2,6 @@ export { defineComponent } from './defineComponent' export { defineAsyncComponent } from './defineAsyncComponent' export { SetupFunction, - SetupContext, ComputedOptions, MethodOptions, ComponentPropsOptions, diff --git a/src/index.ts b/src/index.ts index d7421b0b..3b1632d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,11 @@ export const version = __VERSION__ export * from './apis' export * from './component' -export { getCurrentInstance, ComponentInternalInstance } from './runtimeContext' +export { + getCurrentInstance, + ComponentInternalInstance, + SetupContext, +} from './runtimeContext' export default Plugin diff --git a/src/mixin.ts b/src/mixin.ts index ed9fc19a..d94c9be2 100644 --- a/src/mixin.ts +++ b/src/mixin.ts @@ -1,10 +1,5 @@ import type { VueConstructor } from 'vue' -import { - ComponentInstance, - SetupContext, - SetupFunction, - Data, -} from './component' +import { ComponentInstance, SetupFunction, Data } from './component' import { isRef, isReactive, toRefs, isRaw } from './reactivity' import { isPlainObject, @@ -24,7 +19,11 @@ import { resolveScopedSlots, asVmProperty, } from './utils/instance' -import { getVueConstructor } from './runtimeContext' +import { + getVueConstructor, + SetupContext, + toVue3ComponentInstance, +} from './runtimeContext' import { createObserver, reactive } from './reactivity/reactive' export function mixin(Vue: VueConstructor) { @@ -53,7 +52,9 @@ export function mixin(Vue: VueConstructor) { if (render) { // keep currentInstance accessible for createElement $options.render = function (...args: any): any { - return activateCurrentInstance(vm, () => render.apply(this, args)) + return activateCurrentInstance(toVue3ComponentInstance(vm), () => + render.apply(this, args) + ) } } @@ -85,16 +86,17 @@ export function mixin(Vue: VueConstructor) { function initSetup(vm: ComponentInstance, props: Record = {}) { const setup = vm.$options.setup! const ctx = createSetupContext(vm) + const instance = toVue3ComponentInstance(vm) + instance.setupContext = ctx // fake reactive for `toRefs(props)` def(props, '__ob__', createObserver()) // resolve scopedSlots and slots to functions - // @ts-expect-error resolveScopedSlots(vm, ctx.slots) let binding: ReturnType> | undefined | null - activateCurrentInstance(vm, () => { + activateCurrentInstance(instance, () => { // make props to be fake reactive, this is for `toRefs(props)` binding = setup(props, ctx) }) @@ -105,9 +107,8 @@ export function mixin(Vue: VueConstructor) { const bindingFunc = binding // keep currentInstance accessible for createElement vm.$options.render = () => { - // @ts-expect-error resolveScopedSlots(vm, ctx.slots) - return activateCurrentInstance(vm, () => bindingFunc()) + return activateCurrentInstance(instance, () => bindingFunc()) } return } else if (isPlainObject(binding)) { @@ -228,15 +229,17 @@ export function mixin(Vue: VueConstructor) { }) }) + let propsProxy: any propsReactiveProxy.forEach((key) => { let srcKey = `$${key}` proxy(ctx, key, { get: () => { - const data = reactive({}) + if (propsProxy) return propsProxy + propsProxy = reactive({}) const source = vm[srcKey] for (const attr of Object.keys(source)) { - proxy(data, attr, { + proxy(propsProxy, attr, { get: () => { // to ensure it always return the latest value return vm[srcKey][attr] @@ -244,7 +247,7 @@ export function mixin(Vue: VueConstructor) { }) } - return data + return propsProxy }, set() { __DEV__ && diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts index 02afdedb..d9b5c770 100644 --- a/src/runtimeContext.ts +++ b/src/runtimeContext.ts @@ -134,6 +134,38 @@ export type EmitFn< }[Event] > +export type Slots = Readonly + +export interface SetupContext { + attrs: Data + slots: Slots + emit: EmitFn + /** + * @deprecated not available in Vue 2 + */ + expose: (exposed?: Record) => void + + /** + * @deprecated not available in Vue 3 + */ + readonly parent: ComponentInstance | null + + /** + * @deprecated not available in Vue 3 + */ + readonly root: ComponentInstance + + /** + * @deprecated not available in Vue 3 + */ + readonly listeners: { [key in string]?: Function } + + /** + * @deprecated not available in Vue 3 + */ + readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] } +} + /** * We expose a subset of properties on the internal instance as they are * useful for advanced external libraries and tools. @@ -179,6 +211,11 @@ export declare interface ComponentInternalInstance { * @internal */ scope: EffectScope + + /** + * @internal + */ + setupContext: SetupContext | null } export function getCurrentInstance() { @@ -190,7 +227,7 @@ const instanceMapCache = new WeakMap< ComponentInternalInstance >() -function toVue3ComponentInstance( +export function toVue3ComponentInstance( vm: ComponentInstance ): ComponentInternalInstance { if (instanceMapCache.has(vm)) { diff --git a/src/utils/helper.ts b/src/utils/helper.ts index e60c4758..24b93aad 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -4,6 +4,7 @@ import { ComponentInternalInstance, getCurrentInstance, getVueConstructor, + Slot, } from '../runtimeContext' import { warn } from './utils' @@ -38,8 +39,8 @@ export function isComponentInstance(obj: any) { return Vue && obj instanceof Vue } -export function createSlotProxy(vm: ComponentInstance, slotName: string) { - return (...args: any) => { +export function createSlotProxy(vm: ComponentInstance, slotName: string): Slot { + return ((...args: any) => { if (!vm.$scopedSlots[slotName]) { if (__DEV__) return warn( @@ -50,7 +51,7 @@ export function createSlotProxy(vm: ComponentInstance, slotName: string) { } return vm.$scopedSlots[slotName]!.apply(vm, args) - } + }) as Slot } export function resolveSlots( diff --git a/src/utils/instance.ts b/src/utils/instance.ts index 7626e0d5..8a294fd5 100644 --- a/src/utils/instance.ts +++ b/src/utils/instance.ts @@ -3,7 +3,8 @@ import vmStateManager from './vmStateManager' import { setCurrentInstance, getCurrentInstance, - setCurrentVue2Instance, + ComponentInternalInstance, + InternalSlots, } from '../runtimeContext' import { Ref, isRef, isReactive } from '../apis' import { hasOwn, proxy, warn } from './utils' @@ -102,7 +103,7 @@ export function updateTemplateRef(vm: ComponentInstance) { export function resolveScopedSlots( vm: ComponentInstance, - slotsProxy: { [x: string]: Function } + slotsProxy: InternalSlots ): void { const parentVNode = (vm.$options as any)._parentVnode if (!parentVNode) return @@ -129,14 +130,14 @@ export function resolveScopedSlots( } export function activateCurrentInstance( - vm: ComponentInstance, - fn: (vm_: ComponentInstance) => any, + instance: ComponentInternalInstance, + fn: (instance: ComponentInternalInstance) => any, onError?: (err: Error) => void ) { let preVm = getCurrentInstance() - setCurrentVue2Instance(vm) + setCurrentInstance(instance) try { - return fn(vm) + return fn(instance) } catch (err) { if (onError) { onError(err) diff --git a/test/v3/runtime-core/apiSetupHelpers.spec.ts b/test/v3/runtime-core/apiSetupHelpers.spec.ts new file mode 100644 index 00000000..78b8ae43 --- /dev/null +++ b/test/v3/runtime-core/apiSetupHelpers.spec.ts @@ -0,0 +1,49 @@ +import { + createApp, + defineComponent, + SetupContext, + useAttrs, + useSlots, +} from '../../../src' + +describe('SFC