From 3babae58bbc7fa074ab0ff854c87029f41593f9c Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 30 Oct 2020 17:17:25 -0400 Subject: [PATCH 01/20] new script setup and ref sugar --- active-rfcs/0000-script-setup.md | 821 +++++++++++++++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 active-rfcs/0000-script-setup.md diff --git a/active-rfcs/0000-script-setup.md b/active-rfcs/0000-script-setup.md new file mode 100644 index 00000000..48611985 --- /dev/null +++ b/active-rfcs/0000-script-setup.md @@ -0,0 +1,821 @@ +- 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 + +```html + + + +``` + +**Note:** the SFC compiler also extracts binding metadata from ` +``` + +### Top level bindings are exposed to template + +Any top-level bindings (both variables and imports) declared inside ` + + +``` + +
+Compiled Output + +```html + + + +``` + +**Note:** The SFC compiler also extracts binding metadata from ` +``` + +
+Compiled Output + +```html + +``` +
+ +### Declaring Component Options + +`export default` can still be used inside ` +``` + +
+Compiled Output + +```html + +``` +
+ +### Top level await + +Top level `await` can be used inside ` +``` + +
+Compiled Output + +```html + +``` +
+ +## Ref Syntax + +Code inside ` + + +``` + +`ref: count = 0` is a [labeled statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label) which is syntactically valid in both JS/TS. However, we are using it as a variable declaration here. The compiler will: + +1. Convert it to a proper variable declaration +2. Wrap its initial value with `ref()` +3. Rewrite all references to `count` into `count.value`. + +
+Compiled Output + +```js +import { ref } from 'vue' + +export default { + setup() { + const count = ref(0) + + function inc() { + count.value++ + } + + return { + count, + inc + } + } +} +``` +
+

+ +Note that the syntax is opt-in: all Composition APIs can be used inside ` + + +``` + +### Accessing Raw Ref + +It is common for an external composition function to expect a raw ref object as argument, so we need a way to access the raw underlying ref object for bindings declared via `ref:`. To deal with that, every `ref:` binding will have a corresponding `$`-prefixed counter part that exposes the raw ref: + +```js +ref: count = 1 +console.log($count.value) // 1 + +$count.value++ +console.log(count) // 2 + +watch($count, newCount => { + console.log('new count is: ', newCount) +}) +``` + +
+Compiled Output + +```js +const count = ref(1) +console.log(count.value) // 1 + +count.value++ +console.log(count.value) // 2 + +watch(count, newCount => { + console.log('new count is: ', newCount) +}) +``` +
+ +### Interaction with Non-Literals + +`ref:` will wrap assignment values with `ref()`. If the value is already a ref, it will be returned as-is. This means we can use `ref:` with any function that returns a ref, for example `computed`: + +```js +import { computed } from 'vue' + +ref: count = 0 +ref: plusOne = computed(() => count + 1) +console.log(plusOne) // 1 +``` + +
+Compiled Output + +```js +import { computed, ref } from 'vue' + +const count = ref(0) +// `ref()` around `computed()` is a no-op here since return value +// from `computed()` is already a ref. +const plusOne = ref(computed(() => count.value + 1)) +``` +
+

+ +Or, any custom composition function that returns a ref: + +```js +import { useMyRef } from './composables' + +ref: myRef = useMyRef() +console.log(myRef) // no need for .value +``` + +
+Compiled Output + +```js +import { useMyRef } from './composables' +import { ref } from 'vue' + +// if useMyRef() returns a ref, it will be untouched +// otherwise it's wrapped into a ref +const myRef = ref(useMyRef()) +console.log(myRef.value) +``` +
+

+ +**Note:** if using TypeScript, this behavior creates a typing mismatch which we will discuss in [TypeScript Integration](#typescript-integration) below. + +### Destructuring + +It is common for a composition function to return an object of refs. To declare multiple ref bindings with destructuring, we can use [Destructuring Assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment): + +```js +ref: ({ x, y } = useMouse()) +``` + +
+Compiled Output + +```js +import { ref } from 'vue' + +const { x: __x, y: __x } = useMouse() +const x = ref(__x) +const y = toRef(__y) +``` +
+

+ +**Note:** object destructuring must be wrapped in parens - this is JavaScript's own syntax requirement to avoid ambiguity with a block statement. + +## TypeScript Integration + +### Typing props, slots, and emit + +To type setup arguments like `props`, `slots` and `emit`, simply declare them: + +```html + +``` + +Runtime props and emits declaration is automatically generated from TS typing to remove the need of double declaration and still ensure correct runtime behavior. 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. + +
+Compile Output + +```html + +``` + +Details on runtime props generation: + +- In dev mode, the compiler will try to infer corresponding runtime validation from the types. For example here `msg: String` is inferred from the `msg: string` type. + +- In prod mode, the compiler will generate the array format declaration to reduce bundle size (the props here will be compiled into `['msg']`) + +- The generated props declaration is force casted into `undefined` to ensure the user provided type is used in the emitted code. + +- The emitted code is still TypeScript with valid typing, which can be further processed by other tools. +
+ +### Ref Declaration and Raw Ref Access + +Unlike normal variable declarations, the `ref:` syntax has some special behavior in terms of typing: + +- The declared variable always has the raw value type, regardless of whether the assigned value is a `Ref` type or not (always unwraps) + +- The accompanying `$`-prefixed raw access variable always has a `Ref` type. If the right hand side value type already extends `Ref`, it will be used as-is; otherwise it will be wrapped as `Ref`. + +The following table demonstrates the resulting types of different usage: + +| source | resulting type for `count` | resulting type for `$count` | +|--------|----------------------------|-----------------------------| +|`ref: count = 1`|`number`|`Ref`| +|`ref: count = ref(1)`|`number`|`Ref`| +|`ref: count = computed(() => 1)`|`number`|`ComputedRef`| +|`ref: count = computed({ get:()=>1, set:_=>_ })`|`number`|`WritableComputedRef`| + +How to support this in Vetur is [discussed in the appendix](#ref-typescript-support-implementation-details). + +## Usage alongside normal ` + + +``` + +
+Compile Output + +```js +import { ref } from 'vue' + +performGlobalSideEffect() + +export const named = 1 + +export default { + setup() { + const count = ref(0) + return { + count + } + } +} +``` +
+ +## Usage restrictions + +Due to the difference in module execution semantics, code inside ` +``` + +## Requires dedicated tooling support + +Appropriating the labeled statement syntax creates a semantic mismatch that leads to integration issues with tooling (linter, TypeScript, IDE support). + +This was also one of the primary reservations we had about Svelte 3's design when it was initially proposed. However since then, the Svelte team has managed to provide good tooling/IDE support via its [language tools](https://github.com/sveltejs/language-tools), even for TypeScript. + +Vue's single file component also already requires dedicated tooling like `eslint-plugin-vue` and Vetur. The team has already discussed the technical feasibility of providing such support and there should be no hard technical blocks to make it work. We are confident that we can provide: + +- Special syntax highlight of `ref:` declared variables in Vetur (so that it's more obvious it's a reactive variable) +- Proper type check via Vetur and dedicated command line checker +- Proper linting via `eslint-plugin-vue` + +## Extracting in-component logic + +The `ref:` syntax sugar is only available inside single file components. Different syntax in and out of components makes it difficult to extract and reuse cross-component logic from existing components. + +This is still an issue for Svelte, since Svelte compilation strategy only works inside Svelte components. The generated code assumes a component context and isn't human-maintainable. + +In Vue's case, what we are proposing here is a very thin syntax sugar on top of idiomatic Composition API code. The most important thing to note here is that the code written with the sugar can be easily de-sugared into what a developer would have written without the sugar, and extracted into external JavaScript files for composition. + +Given a piece of code written using the `ref:` sugar, the workflow of extracting it into an external composition function could be: + +1. Select code range for the code to be extracted +2. In VSCode command input: `>vetur de-sugar ref usage' +3. Code gets de-sugared and saved to clipboard +4. Paste code into external file and wrap into an exported function +5. Import the function in original file and replace original code. + +# Alternatives + +## Comment-based syntax + +```html + + + +``` + +## Other related proposals + +- https://github.com/vuejs/rfcs/pull/182 (current ` +``` + +The `bindings` object will be: + +```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 +
{{ 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) +} +``` + +## Ref TypeScript Support Implementation Details + +There are two issues that prevent `ref:` from working out of the box with TypeScript. Given the following code: + +```ts +ref: count = x +``` + +1. TS won't know `count` should be treated as a local variable +2. If `x` has type `Ref`, there will be a type mismatch since we expect to use `count` as `T`. + +The general idea is to pre-transform the code into alternative TypeScript for type checking only (different from runtime-oriented output), get the diagnostics, and map them back. This will be performed by Vetur for IDE intellisense, and via a dedicated command line tool for type checking `*.vue` files (e.g. VTI or `@vuedx/typecheck`). + +Example + +```ts +// source +ref: count = x + +// transformed +import { ref, unref } from 'vue' + +let count = unref(x) +let $count = ref(x) +``` + +`ref` and `unref` here are used solely for type conversion purposes since their signatures are: + +```ts +function ref(value: T): T extends Ref ? T : Ref +function unref(value: T): T extends Ref ? V : T +``` + +For destructuring: + +```ts +// source +ref: ({ foo, bar } = useX()) + +// transformed +import { ref, unref } from 'vue' + +const { foo: __foo, bar: __bar } = useX() +let foo = unref(__foo) +let $foo = ref(__foo) +let bar = unref(__bar) +let $bar = ref(__bar) +``` + +## Svelte Syntax Details + +- `export` is used to created component props [[details](https://svelte.dev/docs#1_export_creates_a_component_prop)] + +- `let` bindings are considered reactive (invalidation calls are automatically injected after assignments to `let` bindings during compilation). [[details](https://svelte.dev/docs#2_Assignments_are_reactive)] + +- Labeled statements starting with `$` are used to denote computed values / reactive statements. [[details](https://svelte.dev/docs#3_$_marks_a_statement_as_reactive)] + +- Imported svelte stores (the loose equivalent of a ref in Vue) can be used like a normal variable by using its `$`-prefixed counterpart. [[details](https://svelte.dev/docs#4_Prefix_stores_with_$_to_access_their_values)] From 2d1bb37b0e68c66b4f758631658b8f3584df495d Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 1 Nov 2020 18:22:26 -0500 Subject: [PATCH 02/20] edits - Provide separate examples for ` + +``` + +
+Compiled Output + +```html + + + +``` + +**Note:** the SFC compiler also extracts binding metadata from ` ``` @@ -41,7 +89,6 @@ console.log($count.value) ```html ``` - -**Note:** the SFC compiler also extracts binding metadata from ` ``` +## Yet Another Way of Doing Things + +Some may think that Vue already has Options API, Composition API, and Class API (outside of core, as a library) - and this RFC is adding yet another way of authoring a component. This is a valid concern, but it does not warrant an instant dismissal. When we talk about the drawbacks of "different ways of doing the same thing", the more fundamental issue is the learning cost incurred when a user encounters code written in another format he/she is not familiar with. It is therefore important to evaluate the addition based on the trade-off between: + +- How much benefit does the new way provide? +- How much learning cost does the new way introduce? + +This is what we did with the Composition API because we believed the scaling benefits provided by Composition API outweighs its learning cost. + +Unlike the relatively significant paradigm difference between Options API and Composition API, this RFC is merely syntax sugar with the primary goal of reducing verbosity. It does not fundamentally alter the mental model. Without the ref sugar, Composition API code inside ` - - -``` - -
-Compiled Output - -```html - - - -``` -
- # Motivation -This proposal has two main goals: - -1. Reduce verbosity of Single File Component ` - - -``` - -`ref: count = 0` is a [labeled statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label) which is syntactically valid in both JS/TS. However, we are using it as a variable declaration here. The compiler will: - -1. Convert it to a proper variable declaration -2. Wrap its initial value with `ref()` -3. Rewrite all references to `count` into `count.value`. - -
-Compiled Output - -```js -import { ref } from 'vue' - -export default { - setup() { - const count = ref(0) - - function inc() { - count.value++ - } - - return { - count, - inc - } - } -} -``` -
-

- -Note that the syntax is opt-in: all Composition APIs can be used inside ` - - -``` - -### Accessing Raw Ref - -It is common for an external composition function to expect a raw ref object as argument, so we need a way to access the raw underlying ref object for bindings declared via `ref:`. To deal with that, every `ref:` binding will have a corresponding `$`-prefixed counter part that exposes the raw ref: - -```js -ref: count = 1 -console.log($count.value) // 1 - -$count.value++ -console.log(count) // 2 - -watch($count, newCount => { - console.log('new count is: ', newCount) -}) -``` - -
-Compiled Output - -```js -const count = ref(1) -console.log(count.value) // 1 - -count.value++ -console.log(count.value) // 2 - -watch(count, newCount => { - console.log('new count is: ', newCount) -}) -``` -
- -### Interaction with Non-Literals - -`ref:` will wrap assignment values with `ref()`. If the value is already a ref, it will be returned as-is. This means we can use `ref:` with any function that returns a ref, for example `computed`: - -```js -import { computed } from 'vue' - -ref: count = 0 -ref: plusOne = computed(() => count + 1) -console.log(plusOne) // 1 -``` - -
-Compiled Output - -```js -import { computed, ref } from 'vue' - -const count = ref(0) -// `ref()` around `computed()` is a no-op here since return value -// from `computed()` is already a ref. -const plusOne = ref(computed(() => count.value + 1)) -``` -
-

- -Or, any custom composition function that returns a ref: - -```js -import { useMyRef } from './composables' - -ref: myRef = useMyRef() -console.log(myRef) // no need for .value -``` - -
-Compiled Output - -```js -import { useMyRef } from './composables' -import { ref } from 'vue' - -// if useMyRef() returns a ref, it will be untouched -// otherwise it's wrapped into a ref -const myRef = ref(useMyRef()) -console.log(myRef.value) -``` -
-

- -**Note:** if using TypeScript, this behavior creates a typing mismatch which we will discuss in [TypeScript Integration](#typescript-integration) below. - -### Destructuring - -It is common for a composition function to return an object of refs. To declare multiple ref bindings with destructuring, we can use [Destructuring Assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment): - -```js -ref: ({ x, y } = useMouse()) -``` - -
-Compiled Output - -```js -import { ref } from 'vue' - -const { x: __x, y: __x } = useMouse() -const x = ref(__x) -const y = toRef(__y) -``` -
-

- -**Note:** object destructuring must be wrapped in parens - this is JavaScript's own syntax requirement to avoid ambiguity with a block statement. - ## TypeScript Integration -### Typing props, slots, and emit - To type setup arguments like `props`, `slots` and `emit`, simply declare them: ```html @@ -527,25 +310,6 @@ Details on runtime props generation: - The emitted code is still TypeScript with valid typing, which can be further processed by other tools.
-### Ref Declaration and Raw Ref Access - -Unlike normal variable declarations, the `ref:` syntax has some special behavior in terms of typing: - -- The declared variable always has the raw value type, regardless of whether the assigned value is a `Ref` type or not (always unwraps) - -- The accompanying `$`-prefixed raw access variable always has a `Ref` type. If the right hand side value type already extends `Ref`, it will be used as-is; otherwise it will be wrapped as `Ref`. - -The following table demonstrates the resulting types of different usage: - -| source | resulting type for `count` | resulting type for `$count` | -|--------|----------------------------|-----------------------------| -|`ref: count = 1`|`number`|`Ref`| -|`ref: count = ref(1)`|`number`|`Ref`| -|`ref: count = computed(() => 1)`|`number`|`ComputedRef`| -|`ref: count = computed({ get:()=>1, set:_=>_ })`|`number`|`WritableComputedRef`| - -How to support this in Vetur is [discussed in the appendix](#ref-typescript-support-implementation-details). - ## Usage alongside normal ` -``` - -## Yet Another Way of Doing Things - -Some may think that Vue already has Options API, Composition API, and Class API (outside of core, as a library) - and this RFC is adding yet another way of authoring a component. This is a valid concern, but it does not warrant an instant dismissal. When we talk about the drawbacks of "different ways of doing the same thing", the more fundamental issue is the learning cost incurred when a user encounters code written in another format he/she is not familiar with. It is therefore important to evaluate the addition based on the trade-off between: - -- How much benefit does the new way provide? -- How much learning cost does the new way introduce? - -This is what we did with the Composition API because we believed the scaling benefits provided by Composition API outweighs its learning cost. - -Unlike the relatively significant paradigm difference between Options API and Composition API, this RFC is merely syntax sugar with the primary goal of reducing verbosity. It does not fundamentally alter the mental model. Without the ref sugar, Composition API code inside ` - - -``` - -## Other related proposals - -- https://github.com/vuejs/rfcs/pull/182 (current `