mirror of https://github.com/vuejs/core.git
				
				
				
			
		
			
				
	
	
		
			261 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			261 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import {
 | |
|   type ComponentInternalOptions,
 | |
|   type ComponentPropsOptions,
 | |
|   EffectScope,
 | |
|   type EmitFn,
 | |
|   type EmitsOptions,
 | |
|   type GenericAppContext,
 | |
|   type GenericComponentInstance,
 | |
|   type LifecycleHook,
 | |
|   type NormalizedPropsOptions,
 | |
|   type ObjectEmitsOptions,
 | |
|   nextUid,
 | |
|   popWarningContext,
 | |
|   pushWarningContext,
 | |
|   warn,
 | |
| } from '@vue/runtime-dom'
 | |
| import { type Block, isBlock } from './block'
 | |
| import { pauseTracking, resetTracking } from '@vue/reactivity'
 | |
| import { EMPTY_OBJ, hasOwn, isFunction } from '@vue/shared'
 | |
| import {
 | |
|   type RawProps,
 | |
|   getPropsProxyHandlers,
 | |
|   normalizePropsOptions,
 | |
| } from './componentProps'
 | |
| import { setDynamicProp } from './dom/prop'
 | |
| import { renderEffect } from './renderEffect'
 | |
| import { emit, normalizeEmitsOptions } from './componentEmits'
 | |
| 
 | |
| export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
 | |
| 
 | |
| export type VaporSetupFn = (
 | |
|   props: any,
 | |
|   ctx: SetupContext,
 | |
| ) => Block | Record<string, any> | undefined
 | |
| 
 | |
| export type FunctionalVaporComponent = VaporSetupFn &
 | |
|   Omit<ObjectVaporComponent, 'setup'> & {
 | |
|     displayName?: string
 | |
|   } & SharedInternalOptions
 | |
| 
 | |
| export interface ObjectVaporComponent
 | |
|   extends ComponentInternalOptions,
 | |
|     SharedInternalOptions {
 | |
|   setup?: VaporSetupFn
 | |
|   inheritAttrs?: boolean
 | |
|   props?: ComponentPropsOptions
 | |
|   emits?: EmitsOptions
 | |
|   render?(ctx: any): Block
 | |
| 
 | |
|   name?: string
 | |
|   vapor?: boolean
 | |
| }
 | |
| 
 | |
| interface SharedInternalOptions {
 | |
|   /**
 | |
|    * Cached normalized props options.
 | |
|    * In vapor mode there are no mixins so normalized options can be cached
 | |
|    * directly on the component
 | |
|    */
 | |
|   __propsOptions?: NormalizedPropsOptions
 | |
|   /**
 | |
|    * Cached normalized props proxy handlers.
 | |
|    */
 | |
|   __propsHandlers?: [ProxyHandler<any> | null, ProxyHandler<any>]
 | |
|   /**
 | |
|    * Cached normalized emits options.
 | |
|    */
 | |
|   __emitsOptions?: ObjectEmitsOptions
 | |
| }
 | |
| 
 | |
| export function createComponent(
 | |
|   component: VaporComponent,
 | |
|   rawProps?: RawProps,
 | |
|   isSingleRoot?: boolean,
 | |
| ): VaporComponentInstance {
 | |
|   // check if we are the single root of the parent
 | |
|   // if yes, inject parent attrs as dynamic props source
 | |
|   if (isSingleRoot && currentInstance && currentInstance.hasFallthrough) {
 | |
|     const attrs = currentInstance.attrs
 | |
|     if (rawProps) {
 | |
|       ;(rawProps.$ || (rawProps.$ = [])).push(() => attrs)
 | |
|     } else {
 | |
|       rawProps = { $: [() => attrs] } as RawProps
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const instance = new VaporComponentInstance(component, rawProps)
 | |
| 
 | |
|   pauseTracking()
 | |
|   let prevInstance = currentInstance
 | |
|   currentInstance = instance
 | |
|   instance.scope.on()
 | |
| 
 | |
|   if (__DEV__) {
 | |
|     pushWarningContext(instance)
 | |
|   }
 | |
| 
 | |
|   const setupFn = isFunction(component) ? component : component.setup
 | |
|   const setupContext = setupFn!.length > 1 ? new SetupContext(instance) : null
 | |
|   const setupResult =
 | |
|     setupFn!(
 | |
|       instance.props,
 | |
|       // @ts-expect-error
 | |
|       setupContext,
 | |
|     ) || EMPTY_OBJ
 | |
| 
 | |
|   if (__DEV__ && !isBlock(setupResult)) {
 | |
|     if (isFunction(component)) {
 | |
|       warn(`Functional vapor component must return a block directly.`)
 | |
|       instance.block = []
 | |
|     } else if (!component.render) {
 | |
|       warn(
 | |
|         `Vapor component setup() returned non-block value, and has no render function.`,
 | |
|       )
 | |
|       instance.block = []
 | |
|     } else {
 | |
|       instance.block = component.render.call(null, setupResult)
 | |
|     }
 | |
|   } else {
 | |
|     // in prod result can only be block
 | |
|     instance.block = setupResult as Block
 | |
|   }
 | |
| 
 | |
|   // single root, inherit attrs
 | |
|   if (
 | |
|     instance.hasFallthrough &&
 | |
|     component.inheritAttrs !== false &&
 | |
|     instance.block instanceof Element &&
 | |
|     Object.keys(instance.attrs).length
 | |
|   ) {
 | |
|     renderEffect(() => {
 | |
|       for (const key in instance.attrs) {
 | |
|         setDynamicProp(instance.block as Element, key, instance.attrs[key])
 | |
|       }
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   if (__DEV__) {
 | |
|     popWarningContext()
 | |
|   }
 | |
| 
 | |
|   instance.scope.off()
 | |
|   currentInstance = prevInstance
 | |
|   resetTracking()
 | |
|   return instance
 | |
| }
 | |
| 
 | |
| export let currentInstance: VaporComponentInstance | null = null
 | |
| 
 | |
| const emptyContext: GenericAppContext = {
 | |
|   app: null as any,
 | |
|   config: {},
 | |
|   provides: /*@__PURE__*/ Object.create(null),
 | |
| }
 | |
| 
 | |
| export class VaporComponentInstance implements GenericComponentInstance {
 | |
|   uid: number
 | |
|   type: VaporComponent
 | |
|   parent: GenericComponentInstance | null
 | |
|   appContext: GenericAppContext
 | |
| 
 | |
|   block: Block
 | |
|   scope: EffectScope
 | |
|   rawProps: RawProps | undefined
 | |
|   props: Record<string, any>
 | |
|   attrs: Record<string, any>
 | |
|   exposed: Record<string, any> | null
 | |
| 
 | |
|   emitted: Record<string, boolean> | null
 | |
|   propsDefaults: Record<string, any> | null
 | |
| 
 | |
|   // for useTemplateRef()
 | |
|   refs: Record<string, any>
 | |
|   // for provide / inject
 | |
|   provides: Record<string, any>
 | |
| 
 | |
|   hasFallthrough: boolean
 | |
| 
 | |
|   isMounted: boolean
 | |
|   isUnmounted: boolean
 | |
|   isDeactivated: boolean
 | |
|   // LifecycleHooks.ERROR_CAPTURED
 | |
|   ec: LifecycleHook
 | |
| 
 | |
|   // dev only
 | |
|   setupState?: Record<string, any>
 | |
|   propsOptions?: NormalizedPropsOptions
 | |
|   emitsOptions?: ObjectEmitsOptions | null
 | |
| 
 | |
|   constructor(comp: VaporComponent, rawProps?: RawProps) {
 | |
|     this.uid = nextUid()
 | |
|     this.type = comp
 | |
|     this.parent = currentInstance
 | |
|     this.appContext = currentInstance
 | |
|       ? currentInstance.appContext
 | |
|       : emptyContext
 | |
| 
 | |
|     this.block = null! // to be set
 | |
|     this.scope = new EffectScope(true)
 | |
| 
 | |
|     this.rawProps = rawProps
 | |
|     this.provides = this.refs = EMPTY_OBJ
 | |
|     this.emitted = this.ec = this.exposed = this.propsDefaults = null
 | |
|     this.isMounted = this.isUnmounted = this.isDeactivated = false
 | |
| 
 | |
|     // init props
 | |
|     const target = rawProps || EMPTY_OBJ
 | |
|     const handlers = getPropsProxyHandlers(comp, this)
 | |
|     this.props = comp.props ? new Proxy(target, handlers[0]!) : {}
 | |
|     this.attrs = new Proxy(target, handlers[1])
 | |
| 
 | |
|     if (__DEV__) {
 | |
|       // cache normalized options for dev only emit check
 | |
|       this.propsOptions = normalizePropsOptions(comp)
 | |
|       this.emitsOptions = normalizeEmitsOptions(comp)
 | |
|     }
 | |
| 
 | |
|     // determine fallthrough
 | |
|     this.hasFallthrough = false
 | |
|     if (rawProps) {
 | |
|       if (rawProps.$ || !comp.props) {
 | |
|         this.hasFallthrough = true
 | |
|       } else {
 | |
|         // check if rawProps contains any keys not declared
 | |
|         const propsOptions = normalizePropsOptions(comp)[0]
 | |
|         for (const key in rawProps) {
 | |
|           if (!hasOwn(propsOptions!, key)) {
 | |
|             this.hasFallthrough = true
 | |
|             break
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // TODO validate props
 | |
|     // TODO init slots
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function isVaporComponent(
 | |
|   value: unknown,
 | |
| ): value is VaporComponentInstance {
 | |
|   return value instanceof VaporComponentInstance
 | |
| }
 | |
| 
 | |
| export class SetupContext<E = EmitsOptions> {
 | |
|   attrs: Record<string, any>
 | |
|   emit: EmitFn<E>
 | |
|   // slots: Readonly<StaticSlots>
 | |
|   expose: (exposed?: Record<string, any>) => void
 | |
| 
 | |
|   constructor(instance: VaporComponentInstance) {
 | |
|     this.attrs = instance.attrs
 | |
|     this.emit = emit.bind(null, instance) as EmitFn<E>
 | |
|     // this.slots = instance.slots
 | |
|     this.expose = (exposed = {}) => {
 | |
|       instance.exposed = exposed
 | |
|     }
 | |
|   }
 | |
| }
 |