diff --git a/active-rfcs/0038-script-setup.md b/active-rfcs/0038-script-setup.md new file mode 100644 index 00000000..ee64dbc9 --- /dev/null +++ b/active-rfcs/0038-script-setup.md @@ -0,0 +1,615 @@ +- Start Date: 2020-10-28 +- Target Major Version: 3.x +- Reference Issues: https://github.com/vuejs/rfcs/pull/182 +- Implementation PR: https://github.com/vuejs/vue-next/pull/2532 + +# Summary + +Introduce a new script type in Single File Components: ` + + +``` + +
+Compiled Output + +```js +import Foo from './Foo.vue' +import { ref } from 'vue' + +export default { + setup() { + const count = ref(1) + const inc = () => { + count.value++ + } + + return function render() { + return h(Foo, { + count, + onClick: inc + }) + } + } +} +``` + +**Note:** the SFC compiler also extracts binding metadata from ` +``` + +# Motivation + +This proposal's main goal is reducing the verbosity of Composition API usage inside Single File Components (SFCs) by directly exposing the context of ` +``` + +### Top level bindings are exposed to template + +When using ` + + +``` + +**Compiled Output:** + +```js +export default { + setup() { + const msg = 'Hello!' + + return function render() { + // has access to everything inside setup() scope + return h('div', msg) + } + } +} +``` + +It is important to notice the different template scoping mental model vs. Options API: when using Options API, the ` + + +``` + +
+Compiled Output + +```js +import Foo from './Foo.vue' +import MyComponent from './MyComponent.vue' + +export default { + setup() { + return function render() { + return [ + h(Foo), + h(MyComponent) + ] + } + } +} +``` + +**Note**: in this case the template compiler has the binding information to generate code that directly use `Foo` from setup bindings instead of dynamically resolving it. + +
+

+ +### Using Dynamic Components + +Since components are referenced as variables instead of registered under string keys, we should use dynamic `:is` binding when using dynamic components inside ` + + +``` + +
+Compiled Output + +```js +import Foo from './Foo.vue' +import Bar from './Bar.vue' + +export default { + setup() { + return function render() { + return [ + h(Foo), + h(someCondition ? Foo : Bar) + ] + } + } +} +``` + +
+ +### Using Directives + +Directives work in a similar fashion - except that a directive named `v-my-dir` will map to a setup scope variable named `vMyDir`: + +```html + + + +``` + +
+Compiled Output + +```js +import { directive as vClickOutside } from 'v-click-outside' + +export default { + setup() { + return function render() { + return withDirectives(h('div'), [ + [vClickOutside] + ]) + } + } +} +``` + +
+ +The reason for requiring the `v` prefix is because it is quite likely for a globally registered directive (e.g. `v-focus`) to clash with a locally declared variable of the same name. The `v` prefix makes the intention of using a variable as a directive more explicit and reduces unintended "shadowing". + +### Declaring `props` and `emits` + +To declare options like `props` and `emits` with full type inference support, we can use the `defineProps` and `defineEmits` APIs, which are automatically avaialbe inside ` +``` + +
+Compiled output + +```js +export default { + props: { + foo: String, + }, + emits: ['change', 'delete'], + setup(props, { emit }) { + // setup code + } +} +``` + +
+ +- `defineProps` and `defineEmits` provides proper type inference based on the options passed. + +- `defineProps` and `defineEmits` are **compiler macros** only usable inside ` +``` + +`useSlots` and `useAttrs` are actual runtime functions that return the equivalent of `setupContext.slots` and `setupContext.attrs`. They can be used in normal composition API functions as well. + +### Type-only props/emit declarations + +Props and emits can also be declared using pure-type syntax by passing a literal type argument to `defineProps` or `defineEmits`: + +```ts +const props = defineProps<{ + foo: string + bar?: number +}>() + +const emit = defineEmits<{ + (e: 'change', id: number): void + (e: 'update', value: string): void +}>() +``` + +- `defineProps` or `defineEmits` can only use either runtime declaration OR type declaration. Using both at the same time will result in a compile error. + +- When using type declaration, equivalent runtime declaration is automatically generated from static analysis to remove the need of double declaration and still ensure correct runtime behavior. + + - In dev mode, the compiler will try to infer corresponding runtime validation from the types. For example here `foo: String` is inferred from the `foo: string` type. If the type is a reference to an imported type, the inferred result will be `foo: null` (equal to `any` type) since the compiler does not have information of external files. + + - In prod mode, the compiler will generate the array format declaration to reduce bundle size (the props here will be compiled into `['msg']`) + + - The emitted code is still TypeScript with valid typing, which can be further processed by other tools. + +- As of now, the type declaration argument must be one of the following to ensure correct static analysis: + + - A type literal + - A reference to a an interface or a type literal in the same file + + Currently complex types and type imports from other files are not supported. It is theoretically possible to support type imports in the future. + +### Default props values when using type declaration + +One drawback of the type-only `defineProps` declaration is that it doesn't have a way to provide default values for the props. To resolve this problem, a `withDefaults` compiler macro is also provided: + +```ts +interface Props { + msg?: string +} + +const props = withDefaults(defineProps(), { + msg: 'hello' +}) +``` + +This will be compiled to equivalent runtime props `default` options. In addition, the `withDefaults` helper provides type checks for the default values, and ensures the returned `props` type has the optional flags removed for properties that do have default values declared. + +### Top level await + +Top level `await` can be used inside ` +``` + +In addition, the awaited expression will be automatically wrapped with a `withAsyncContext` helper. The helper saves and restores the current instance context so that the current instance context is preserved even after the `await` statement: + +```js +import { withAsyncContext } from 'vue' + +export default { + async setup() { + const post = await withAsyncContext( + fetch(`/api/post/1`).then((r) => r.json()) + ) + + // current instance context preserved + // e.g. onMounted() will still work. + + return { post } + } +} +``` + +Relevant: https://github.com/vuejs/rfcs/issues/234 + +### Exposing component's public interface + +In a traditional Vue component, everything exposed to the template is implicitly exposed on the component instance, which can be retrieved by a parent component via template refs. That is to say, up to this point the **template render context** and the **imperative public interface** of a component is one and the same. We have found this to be problematic because the two use cases do not always align perfectly. In fact, most of the time we are over-exposing on the public interface front. This is why we are discussing an explicit way to define a component's imperative public interface in the [Expose RFC](https://github.com/vuejs/rfcs/pull/210). + +With ` +``` + +When a parent gets an instance of this component via template refs, the retrieved instance will be of the shape `{ a: number, b: number }` (refs are automatically unwrapped just like on normal instances). + +This will be compiled into the runtime equivalent as proposed in the [Expose RFC](https://github.com/vuejs/rfcs/pull/210). + +## Usage alongside normal ` + + +``` + +
+Compile Output + +```js +import { ref } from 'vue' + +performGlobalSideEffect() + +export const named = 1 + +export default { + setup() { + const count = ref(0) + return { + count, + } + }, +} +``` + +
+ +## Automatic `name` Inference + +Vue 3 SFCs automatically infers the component's name from the component's **filename** in the following cases: + +- Dev warning formatting +- DevTools inspection +- Recursive self-reference. E.g. a file named `FooBar.vue` can refer to itself as `` in its template. + + This has lower priority than explicity registered/imported components. If you have a named import that conflicts with the component's inferred name, you can alias it: + + ```js + import { FooBar as FooBarChild } from './components' + ``` + +In most cases, explicit `name` declaration is not needed. The only cases where you do need it is when you need the `name` for `` inclusion / exclusion or direct inspection of the component's options. + +## Declaring Additional Options + +The ` + + +``` + +## Usage restrictions + +Due to the difference in module execution semantics, code inside ` +``` + +The `bindings` object will be: + +```js +{ + foo: 'setup-const', + bar: 'props' +} +``` + +This object can then be passed to the template compiler: + +```js +import { compile } from '@vue/compiler-dom' + +compile(template, { + bindingMetadata: bindings, +}) +``` + +With the binding metadata available, the template compiler can generate code that directly access template variables from the corresponding source, without having to go through the render context proxy: + +```html +
{{ foo + bar }}
+``` + +```js +// code generated without bindingMetadata +// here _ctx is a Proxy object that dynamically dispatches property access +function render(_ctx) { + return createVNode('div', null, _ctx.foo + _ctx.bar) +} + +// code generated with bindingMetadata +// bypasses the render context proxy +function render(_ctx, _cache, $setup, $props, $data) { + return createVNode('div', null, $setup.foo + $props.bar) +} +``` + +The binding information is also used in inline template mode to generate more efficient code.