Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SFC Improvements #182

Closed
wants to merge 14 commits into from
132 changes: 0 additions & 132 deletions active-rfcs/0000-sfc-component-sugar.md

This file was deleted.

90 changes: 90 additions & 0 deletions active-rfcs/0000-sfc-script-setup.md
Expand Up @@ -98,6 +98,24 @@ export default {
}
```

## Exposing Components

Exports from `<script setup>` are also available to the template when rendering components. For example:

```vue
<script setup>
export { default as Foo } from './Foo.vue'
export { default as Bar } from './Bar.vue'
export const ok = Math.random()
</script>

<template>
<Foo/>
<Bar/>
<component :is="ok ? Foo : Bar"/>
</template>
```

## Declaring props or additional options

One problem with `<script setup>` is that it removes the ability to declare other component options, for example `props`. We can solve this by treating the default export as additional options (this also aligns with normal `<script>`):
Expand Down Expand Up @@ -194,6 +212,7 @@ export default defineComponent({

- The emitted code is still TypeScript with valid typing, which can be further processed by other tools.

Note that the `props` type declaration value cannot be an imported type, because the SFC compiler does not process external files to extract the prop names.

## Usage alongside normal `<script>`

Expand Down Expand Up @@ -239,6 +258,77 @@ export function setup() {
export default { setup }
```

## Transform API

The `@vue/compiler-sfc` package exposes the `compileScript` method for processing `<script setup>`:

```js
import { parse, compileScript } from '@vue/compiler-sfc'

const descriptor = parse(`...`)

if (descriptor.scriptSetup) {
const result = compileScript(descriptor) // returns SFCScriptBlock
console.log(result.code)
console.log(result.bindings) // see next section
}
```

The compilation requires the entire descriptor to be provided, and the resulting code will include sources from both `<script setup>` and normal `<script>` (if present). It is the higher level tools' (e.g. `vite` or `vue-loader`) responsibility to properly assemble the compiled output.

## Template Binding Optimization

The `SFCScriptBlock` returned by `compiledScript` also exposes a `bindings` object, which is the exported binding metadata gathered during the compilation. For example, given the following `<script setup>`:

```vue
<script setup="props">
export const foo = 1

export default {
props: ['bar']
}
</script>
```

The `bindings` object will be:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yyx990803 I looked up the BindingMetadata typedef in compiler-core. Following types are defined: 'data' | 'props' | 'setup' | 'options'

I think an additional value A: 'import', for export { A } from './a' could lead to interesting optimizations.

The compiler would then be free to reference the import directly, without reactivity and bypassing the setup state for references to A.

That's particularly interesting for local components (or directives).
This RFC also looks for a syntax to import local components. With the ability to recognize imports (constants) and reference them directly, it doesn't need one!

If the template does <MyButton /> and the script setup contains an export { MyButton } from './my-button' identified as such, the compiler could produce the equivalent of h(MyButton, ...).

Without this knowledge, the local component would suffer two drawbacks:

  • going through the reactivity layer to access MyButton on the setup object.
  • assuming that MyButton is a variable that could change, so the component is dynamic and precludes the static optimizations.

Copy link
Member Author

@yyx990803 yyx990803 Sep 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason we are not doing this is that the template and script parts of an SFC are typically executed as separate modules to
1. allow each having its own loader pipelines (webpack specific)
2. allow template to be individually hot-reloaded (thus preserving component state).

Regarding the drawbacks:

  • Component access only goes through the setup object, which is not a full reactive object (it's a proxy that only checks for ref unwrapping, so the cost is fairly cheap).

  • <MyButton/> directly compiles to h($setup.MyButton), so there is no dynamic assumptions here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<MyButton/> directly compiles to h($setup.MyButton), so there is no dynamic assumptions here.

I think I'm missing a piece here.

Setup properties can mutate, right? Do you know/assume $setup.MyButton is a constant rather than a reactive value that will change?

If you assume MyButton can mutate, then isn't the code equivalent to <component :is="MyButton">, which precludes some optimizations compared to a static <MyButton>?
https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Ccomponent%20%3Ais%3D%5C%22xy%5C%22%20%2F%3E%5Cr%5Cn%3Cmy-xy%20%2F%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22optimizeImports%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup%22%2C%22foo%22%3A%22setup%22%2C%22bar%22%3A%22props%22%7D%2C%22optimizeBindings%22%3Afalse%7D%7D

Or does the syntax <MyButton> imply that it must be static, even though it comes from setup?
In this case, what happens if that assumption is violated by user, do you emit a warning in DEV?


```js
{
foo: 'setup',
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
<div>{{ foo + bar }}</div>
```

```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)
}
```

## Usage Restrictions

Due to the difference in module execution semantics, code inside `<script setup>` relies on the context of an SFC. When moved into external `.js` or `.ts` files, it may lead to confusions for both developers and tools. Therefore, **`<script setup>`** cannot be used with the `src` attribute.
Expand Down