From 1a7e65e8fb5d319aea70140be1b743b5ea97e327 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 4 Aug 2021 16:36:55 -0400 Subject: [PATCH 1/2] ref sugar (take 2) --- active-rfcs/0000-ref-sugar.md | 342 ++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 active-rfcs/0000-ref-sugar.md diff --git a/active-rfcs/0000-ref-sugar.md b/active-rfcs/0000-ref-sugar.md new file mode 100644 index 00000000..071dc750 --- /dev/null +++ b/active-rfcs/0000-ref-sugar.md @@ -0,0 +1,342 @@ +- Start Date: 2021-07-16 +- Target Major Version: 3.x +- Reference Issues: https://github.com/vuejs/rfcs/pull/182, https://github.com/vuejs/rfcs/pull/228 + +# Summary + +Introduce a compiler-based syntax sugar for using refs without `.value`. + +# Basic example + +```html + + + +``` + +
+Compiled Output + +```js +import { ref } from 'vue' + +export default { + setup() { + const count = ref(1) + + console.log(count.value) + + function inc() { + count.value++ + } + + return () => { + return h('button', { onClick: inc }, count.value) + } + } +} +``` + +
+ +# Motivation + +Ever since the introduction of the Composition API, one of the primary unresolved questions is the use of refs vs. reactive objects. It can be cumbersome to use `.value` everywhere, and it is easy to miss if not using a type system. Some users specifically lean towards using `reactive()` exclusively so that they don't have to deal with refs. + +This proposal aims to improve the ergonomics of refs with a set of compile-time macros. + +# Detailed design + +## Overview + +- Declare reactive variables with `$ref` +- Declare reactively-derived variables with `$computed` +- Destructure reactive variables from an object with `$fromRefs` +- Get the raw ref object of a `$ref`-declared variable with `$raw` + +## `$ref` + +```js +let count = $ref(0) + +function inc() { + count++ +} +``` + +Compiled Output: + +```js +let count = ref(0) + +function inc() { + count.value++ +} +``` + +Variables declared with `$ref` can be accessed or mutated just like normal variables - but it enables reactivity for these operations. The `$ref` serves as a hint for the compiler to append `.value` to all references to the variable. + +**Notes** + +- `$ref` is a compile-time macro and does not need to be imported. +- `$ref` can only be used with `let` because it would be pointless to declare a constant ref. +- `$ref` can also be used to create a variable-like binding for other ref types, e.g. `computed`, `shallowRef` or even `customRef`: + + ```js + let count = $ref(0) + + const plusOne = $ref(computed(() => count + 1)) + console.log(plusOne) + ``` + +## `$computed` + +Because `computed` is so commonly used, it also has its dedicated macro: + +```diff +- const plusOne = $ref(computed(() => count + 1)) ++ const plusOne = $computed(() => count + 1) +``` + +## Destructuring with `$fromRefs` + +It is common for a composition function to return an object of refs. To declare multiple ref bindings with destructuring, we can use the `$fromRefs` macro: + +```js +const { x, y, method } = $fromRefs(useFoo()) + +console.log(x, y) +method() +``` + +Compiled Output: + +```js +import { shallowRef } from 'vue' + +const { x: __x, y: __y, method: __method } = useFoo() +const x = shallowRef(__x) +const y = shallowRef(__y) +const method = shallowRef(__method) + +console.log(x.value, y.value) +method.value() +``` + +Note this works even if a property is not a ref: for example the `method` property is a plain function here - `shallowRef` will wrap it as an actual ref so that the rest of the code could work as expected. + +## Accessing Raw Refs with `$raw` + +In some cases we may need to access the underlying raw ref object of a `$ref`-declared variable. We can do that with the `$raw` macro: + +```js +let count = $ref(0) + +const countRef = $raw(count) + +fnThatExpectsRef(countRef) +``` + +Compiled Output: + +```js +let count = ref(0) + +const countRef = count + +fnThatExpectsRef(countRef) +``` + +Think of `$raw` as "do not append `.value` to anything passed to me, and return it". This means it can also be used on an object containing `$ref` variables: + +```js +let x = $ref(0) +let y = $ref(0) + +const coords = $raw({ x, y }) + +console.log(coords.x.value) +``` + +## TypeScript & Tooling Integration + +Vue will provide typings for these macros (available globally) and all types will work as expected. There are no incompatibilities with standard TypeScript semantics so the syntax would work with all existing tooling. + +## Ref Usage in Nested Function Scopes + +> This section isn't implemented as of now + +Technically, `$ref` doesn't have to be limited to root level scope and can be used anywhere `let` declarations can be used, including nested function scope: + +```js +function useMouse() { + let x = $ref(0) + let y = $ref(0) + + function update(e) { + x = e.pageX + y = e.pageY + } + + onMounted(() => window.addEventListener('mousemove', update)) + onUnmounted(() => window.removeEventListener('mousemove', update)) + + return $raw({ + x, + y + }) +} +``` + +
+Compiled Output + +```js +function useMouse() { + let x = ref(0) + let y = ref(0) + + function update(e) { + x.value = e.pageX + y.value = e.pageY + } + + onMounted(() => window.addEventListener('mousemove', update)) + onUnmounted(() => window.removeEventListener('mousemove', update)) + + return { + x, + y + } +} +``` + +
+

+ +## Implementation Status + +This proposal is currently implemented in 3.2.0-beta as an experimental feature. + +- It is currently only usable in `