]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: setup context + emit
authorEvan You <yyx990803@gmail.com>
Wed, 19 Jun 2019 08:43:34 +0000 (16:43 +0800)
committerEvan You <yyx990803@gmail.com>
Wed, 19 Jun 2019 08:43:34 +0000 (16:43 +0800)
packages/runtime-core/__tests__/createComponent.spec.tsx
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-dom/src/modules/events.ts
packages/shared/src/index.ts

index 885b58dec5a7ffb803ef92404c3a974ab0e13562..7c81aadf1c68c3bc7a043d7f4a4f70fb50330ab0 100644 (file)
@@ -68,12 +68,11 @@ test('type inference w/ optional props declaration', () => {
         a: 1
       }
     },
-    render(props) {
-      props.msg
+    render(ctx) {
+      ctx.msg
+      ctx.a * 2
+      this.msg
       this.a * 2
-      // should not make state and this indexable
-      // state.foobar
-      // this.foobar
     }
   })
   ;(<Comp msg="hello"/>)
@@ -96,9 +95,10 @@ test('type inference w/ array props declaration', () => {
         c: 1
       }
     },
-    render(props) {
-      props.a
-      props.b
+    render(ctx) {
+      ctx.a
+      ctx.b
+      ctx.c
       this.a
       this.b
       this.c
index a92951c3b74ccfa513b34f2b91d2886fabab3f72..17e5cd810cc749a93a383a2ddfb4870bc25fcaff 100644 (file)
@@ -5,7 +5,7 @@ import {
   state,
   immutableState
 } from '@vue/reactivity'
-import { EMPTY_OBJ, isFunction } from '@vue/shared'
+import { EMPTY_OBJ, isFunction, capitalize, invokeHandlers } from '@vue/shared'
 import { RenderProxyHandlers } from './componentProxy'
 import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
 import { PROPS, DYNAMIC_SLOTS, FULL_PROPS } from './patchFlags'
@@ -24,33 +24,26 @@ export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
   $slots: Data
   $root: ComponentInstance | null
   $parent: ComponentInstance | null
+  $emit: (event: string, ...args: any[]) => void
 } & P &
   S
 
-type RenderFunction<P = Data> = (
-  props: P,
-  slots: Slots,
-  attrs: Data,
-  vnode: VNode
-) => any
+type SetupFunction<Props, RawBindings> = (
+  props: Props,
+  ctx: SetupContext
+) => RawBindings | (() => VNodeChild)
 
-type RenderFunctionWithThis<Props, RawBindings> = <
+type RenderFunction<Props = {}, RawBindings = {}> = <
   Bindings extends UnwrapValue<RawBindings>
 >(
   this: ComponentRenderProxy<Props, Bindings>,
-  props: Props,
-  slots: Slots,
-  attrs: Data,
-  vnode: VNode
+  ctx: ComponentRenderProxy<Props, Bindings>
 ) => VNodeChild
 
 interface ComponentOptionsWithoutProps<Props = Data, RawBindings = Data> {
   props?: undefined
-  setup?: (
-    this: ComponentRenderProxy<Props>,
-    props: Props
-  ) => RawBindings | RenderFunction<Props>
-  render?: RenderFunctionWithThis<Props, RawBindings>
+  setup?: SetupFunction<Props, RawBindings>
+  render?: RenderFunction<Props, RawBindings>
 }
 
 interface ComponentOptionsWithArrayProps<
@@ -59,11 +52,8 @@ interface ComponentOptionsWithArrayProps<
   Props = { [key in PropNames]?: any }
 > {
   props: PropNames[]
-  setup?: (
-    this: ComponentRenderProxy<Props>,
-    props: Props
-  ) => RawBindings | RenderFunction<Props>
-  render?: RenderFunctionWithThis<Props, RawBindings>
+  setup?: SetupFunction<Props, RawBindings>
+  render?: RenderFunction<Props, RawBindings>
 }
 
 interface ComponentOptionsWithProps<
@@ -72,11 +62,8 @@ interface ComponentOptionsWithProps<
   Props = ExtractPropTypes<PropsOptions>
 > {
   props: PropsOptions
-  setup?: (
-    this: ComponentRenderProxy<Props>,
-    props: Props
-  ) => RawBindings | RenderFunction<Props>
-  render?: RenderFunctionWithThis<Props, RawBindings>
+  setup?: SetupFunction<Props, RawBindings>
+  render?: RenderFunction<Props, RawBindings>
 }
 
 export type ComponentOptions =
@@ -84,14 +71,15 @@ export type ComponentOptions =
   | ComponentOptionsWithoutProps
   | ComponentOptionsWithArrayProps
 
-export interface FunctionalComponent<P = {}> extends RenderFunction<P> {
+export interface FunctionalComponent<P = {}> {
+  (props: P, ctx: SetupContext): VNodeChild
   props?: ComponentPropsOptions<P>
   displayName?: string
 }
 
 type LifecycleHook = Function[] | null
 
-export interface LifecycleHooks {
+interface LifecycleHooks {
   bm: LifecycleHook // beforeMount
   m: LifecycleHook // mounted
   bu: LifecycleHook // beforeUpdate
@@ -105,6 +93,13 @@ export interface LifecycleHooks {
   ec: LifecycleHook // errorCaptured
 }
 
+interface SetupContext {
+  attrs: Data
+  slots: Slots
+  refs: Data
+  emit: ((event: string, ...args: any[]) => void)
+}
+
 export type ComponentInstance<P = Data, S = Data> = {
   type: FunctionalComponent | ComponentOptions
   parent: ComponentInstance | null
@@ -114,22 +109,22 @@ export type ComponentInstance<P = Data, S = Data> = {
   subTree: VNode
   update: ReactiveEffect
   effects: ReactiveEffect[] | null
-  render: RenderFunction<P> | null
+  render: RenderFunction<P, S> | null
+
   // the rest are only for stateful components
-  renderProxy: ComponentRenderProxy | null
-  propsProxy: Data | null
   state: S
   props: P
-  attrs: Data
-  slots: Slots
-  refs: Data
-} & LifecycleHooks
+  renderProxy: ComponentRenderProxy | null
+  propsProxy: P | null
+  setupContext: SetupContext | null
+} & SetupContext &
+  LifecycleHooks
 
 // createComponent
 // overload 1: direct setup function
 // (uses user defined props interface)
 export function createComponent<Props>(
-  setup: (props: Props) => RenderFunction<Props>
+  setup: (props: Props, ctx: SetupContext) => (() => any)
 ): (props: Props) => any
 // overload 2: object format with no props
 // (uses user defined props interface)
@@ -182,6 +177,7 @@ export function createComponentInstance(
     render: null,
     renderProxy: null,
     propsProxy: null,
+    setupContext: null,
 
     bm: null,
     m: null,
@@ -201,7 +197,15 @@ export function createComponentInstance(
     props: EMPTY_OBJ,
     attrs: EMPTY_OBJ,
     slots: EMPTY_OBJ,
-    refs: EMPTY_OBJ
+    refs: EMPTY_OBJ,
+
+    emit: (event: string, ...args: any[]) => {
+      const props = instance.vnode.props || EMPTY_OBJ
+      const handler = props[`on${event}`] || props[`on${capitalize(event)}`]
+      if (handler) {
+        invokeHandlers(handler, args)
+      }
+    }
   }
 
   instance.root = parent ? parent.root : instance
@@ -223,12 +227,13 @@ export function setupStatefulComponent(instance: ComponentInstance) {
     currentInstance = instance
     // the props proxy makes the props object passed to setup() reactive
     // so props change can be tracked by watchers
-    // only need to create it if setup() actually expects it
     // it will be updated in resolveProps() on updates before render
     const propsProxy = (instance.propsProxy = setup.length
       ? immutableState(instance.props)
       : null)
-    const setupResult = setup.call(proxy, propsProxy)
+    const setupContext = (instance.setupContext =
+      setup.length > 1 ? createSetupContext(instance) : null)
+    const setupResult = setup.call(proxy, propsProxy, setupContext)
     if (isFunction(setupResult)) {
       // setup returned an inline render function
       instance.render = setupResult
@@ -245,22 +250,55 @@ export function setupStatefulComponent(instance: ComponentInstance) {
   }
 }
 
+const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
+;['attrs', 'slots', 'refs'].forEach((type: string) => {
+  SetupProxyHandlers[type] = {
+    get: (instance: any, key: string) => (instance[type] as any)[key],
+    has: (instance: any, key: string) => key in (instance[type] as any),
+    ownKeys: (instance: any) => Object.keys(instance[type] as any),
+    set: () => false,
+    deleteProperty: () => false
+  }
+})
+
+function createSetupContext(instance: ComponentInstance): SetupContext {
+  const context = {
+    // attrs, slots & refs are non-reactive, but they need to always expose
+    // the latest values (instance.xxx may get replaced during updates) so we
+    // need to expose them through a proxy
+    attrs: new Proxy(instance, SetupProxyHandlers.attrs),
+    slots: new Proxy(instance, SetupProxyHandlers.slots),
+    refs: new Proxy(instance, SetupProxyHandlers.refs),
+    emit: instance.emit
+  } as any
+  return __DEV__ ? Object.freeze(context) : context
+}
+
 export function renderComponentRoot(instance: ComponentInstance): VNode {
-  const { type: Component, renderProxy, props, slots, attrs, vnode } = instance
+  const {
+    type: Component,
+    vnode,
+    renderProxy,
+    setupContext,
+    props,
+    slots,
+    attrs,
+    refs,
+    emit
+  } = instance
   if (vnode.shapeFlag & STATEFUL_COMPONENT) {
     return normalizeVNode(
-      (instance.render as RenderFunction).call(
-        renderProxy,
-        props,
-        slots,
-        attrs,
-        vnode
-      )
+      (instance.render as RenderFunction).call(renderProxy, props, setupContext)
     )
   } else {
     // functional
     return normalizeVNode(
-      (Component as FunctionalComponent)(props, slots, attrs, vnode)
+      (Component as FunctionalComponent)(props, {
+        attrs,
+        slots,
+        refs,
+        emit
+      })
     )
   }
 }
index 4d30a9bec6f80cb33b9b0b16c5b62f9bf88090ba..90b75be1a3ca08b3cb73ee53de2dbb4c47b87708 100644 (file)
@@ -25,6 +25,8 @@ export const RenderProxyHandlers = {
           return target.root
         case '$el':
           return target.vnode && target.vnode.el
+        case '$emit':
+          return target.emit
         default:
           break
       }
index 92b5914e7c1a91b97b1ba9e6ffe4ff9d030124aa..60c9136eac43ef3274a62a50fff5a4d5311e1587 100644 (file)
@@ -1,3 +1,5 @@
+import { invokeHandlers } from '@vue/shared'
+
 interface Invoker extends Function {
   value: EventValue
   lastUpdated?: number
@@ -56,30 +58,18 @@ export function patchEvent(
 
 function createInvoker(value: any) {
   const invoker = ((e: Event) => {
-    invokeEvents(e, invoker.value, invoker.lastUpdated)
+    // async edge case #6566: inner click event triggers patch, event handler
+    // attached to outer element during patch, and triggered again. This
+    // happens because browsers fire microtask ticks between event propagation.
+    // the solution is simple: we save the timestamp when a handler is attached,
+    // and the handler would only fire if the event passed to it was fired
+    // AFTER it was attached.
+    if (e.timeStamp >= invoker.lastUpdated) {
+      invokeHandlers(invoker.value, [e])
+    }
   }) as any
   invoker.value = value
   value.invoker = invoker
   invoker.lastUpdated = getNow()
   return invoker
 }
-
-function invokeEvents(e: Event, value: EventValue, lastUpdated: number) {
-  // async edge case #6566: inner click event triggers patch, event handler
-  // attached to outer element during patch, and triggered again. This
-  // happens because browsers fire microtask ticks between event propagation.
-  // the solution is simple: we save the timestamp when a handler is attached,
-  // and the handler would only fire if the event passed to it was fired
-  // AFTER it was attached.
-  if (e.timeStamp < lastUpdated) {
-    return
-  }
-
-  if (Array.isArray(value)) {
-    for (let i = 0; i < value.length; i++) {
-      value[i](e)
-    }
-  } else {
-    value(e)
-  }
-}
index 1884ae88f949105aafbf79ae25ffe88b91c0e95a..3e670db312ff6124055a1543852068853510ed57 100644 (file)
@@ -30,3 +30,16 @@ export const hyphenate = (str: string): string => {
 export const capitalize = (str: string): string => {
   return str.charAt(0).toUpperCase() + str.slice(1)
 }
+
+export function invokeHandlers(
+  handlers: Function | Function[],
+  args: any[] = EMPTY_ARR
+) {
+  if (isArray(handlers)) {
+    for (let i = 0; i < handlers.length; i++) {
+      handlers[i].apply(null, args)
+    }
+  } else {
+    handlers.apply(null, args)
+  }
+}