From 3538d5d0637ac70e1de88d7640a37e15808df61e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 26 Mar 2019 07:07:54 -0400 Subject: [PATCH 1/3] advanced reactivity API --- active-rfcs/0000-advanced-reactivity-api.md | 302 ++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 active-rfcs/0000-advanced-reactivity-api.md diff --git a/active-rfcs/0000-advanced-reactivity-api.md b/active-rfcs/0000-advanced-reactivity-api.md new file mode 100644 index 00000000..f8fda5d3 --- /dev/null +++ b/active-rfcs/0000-advanced-reactivity-api.md @@ -0,0 +1,302 @@ +- Start Date: 03-05-2019 +- Target Major Version: 2.x & 3.x +- Reference Issues: N/A +- Implementation PR: N/A + +# Summary + +Provide standalone APIs for creating and observing reactive state. + +# Basic example + +```js +import { state, value, computed, watch } from '@vue/observer' + +// reactive object +// equivalent of 2.x Vue.observable() +const obj = state({ a: 1 }) + +// watch with a getter function +watch(() => obj.a, value => { + console.log(`obj.a is: ${value}`) +}) + +// a "ref" object that has a .value property +const count = value(0) + +// computed "ref" with a read-only .value property +const plusOne = computed(() => count.value + 1) + +// refs can be watched directly +watch(count, (count, oldCount) => { + console.log(`count is: ${count}`) +}) + +watch(plusOne, countPlusOne => { + console.log(`count plus one is: ${countPlusOne}`) +}) +``` + +# Motivation + +## Decouple the reactivity system from component instances + +Vue's reactivity system powers a few aspects of Vue: + +- Tracking dependencies used during a component's render for automatic component re-render + +- Tracking dependencies of computed properties to only re-compute values when necessary + +- Expose `this.$watch` API for users to perform custom side effects in response to state changes + +Until 2.6, the reactivity system has largely been considered an internal implementation, and there is no dedicated API for creating / watching reactive state without doing it inside a component instance. + +However, such coupling isn't technically necessary. In 3.x we've already split the reactivity system into its own package (`@vue/observer`) with dedicated APIs, so it makes sense to also expose these APIs to enable more advanced use cases. + +With these APIs it becomes possible to encapsulate stateful logic and side effects without components involved. In addition, with proper ability to "connect" the created state back into component instances, they also unlock a powerful component logic reuse mechanism. + +# Detailed design + +## Reactive Objects + +In 2.6 we introduced the `observable` API for creating reactive objects. We've noticed the naming causes confusion for some users who are familiar with RxJS or reactive programming where the term "observable" is commonly used to denote event streams. So here we intend to rename it to simply `state`: + +``` js +import { state } from 'vue' + +const object = state({ + count: 0 +}) +``` + +This works exactly like 2.6 `Vue.observable`. The returned object behaves just like a normal object, and when its properties are accessed in **reactive computations** (render functions, computed property getters and watcher getters), they are tracked as dependencies. Mutation to these properties will cause corresponding computations to re-run. + +## Value Refs + +The `state` API cannot be used for primitive values because: + +- Vue tracks dependencies by intercepting property accesses. Usage of primitive values in reactive computations cannot be tracked. + +- JavaScript values are not passed by reference. Passing a value directly means the receiving function will not be able to read the latest value when the original is mutated. + +The simple solution is wrapping the value in an object wrapper that can be passed around by reference. This is exactly what the `value` API does: + +``` js +import { value } from 'vue' + +const countRef = value(0) +``` + +The `value` API creates a wrapper object for a value, called a **ref**. A ref is a reactive object with a single property: `.value`. The property points to the actual value being held and is writable: + +``` js +// read the value +console.log(countRef.value) // 0 + +// mutate the value +countRef.value++ +``` + +Refs are primarily used for holding primitive values, but it can also hold any other values including deeply nested objects and arrays. Non-primitive values held inside a ref behave like normal reactive objects created via `state`. + +## Computed Refs + +In addition to plain value refs, we can also create computed refs: + +``` js +import { value, computed } from 'vue' + +const count = value(0) +const countPlusOne = computed(() => count.value + 1) + +console.log(countPlusOne.value) // 1 +count.value++ +console.log(countPlusOne.value) // 2 +``` + +Computed refs are readonly by default - assigning to its `value` property will result in an error. + +Computed refs can be made writable by passing a write callback as the 2nd argument: + +``` js +const writableRef = computed( + // read + () => count.value + 1, + // write + val => { + count.value = val - 1 + } +) +``` + +Computed refs behaves like computed properties in a component: it tracks its dependencies and only re-evaluates when dependencies have changed. + +## Watchers + +All `.value` access are reactive, and can be tracked with the standalone `watch` API. + +**NOTE: unlike 2.x, the `watch` API is immediate by default.** + +`watch` can be called with a single function. The function will be called immediately, and will be called again whenever dependencies change: + +``` js +import { value, watch } from 'vue' + +const count = value(0) + +// watch and re-run the effect +watch(() => { + console.log('count is: ', count.value) +}) +// -> count is: 0 + +count.value++ +// -> count is: 1 +``` + +### Watch with a Getter + +When using a single function, any reactive properties accessed during its execution are tracked as dependencies. The **computation** and the **side effect** are performed together. To separate the two, we can pass two functions instead: + +``` js +watch( + // 1st argument (the "computation", or getter) should return a value + () => count.value + 1, + // 2nd argument (the "effect", or callback) only fires when value returned + // from the getter changes + value => { + console.log('count + 1 is: ', value) + } +) +// -> count + 1 is: 1 + +count.value++ +// -> count + 1 is: 2 +``` + +### Watching Refs + +The 1st argument can also be a ref: + +``` js +// double is a computed ref +const double = computed(() => count.value * 2) + +// watch a ref directly +watch(double, value => { + console.log('double the count is: ', value) +}) +// -> double the count is: 0 + +count.value++ +// -> double the count is: 2 +``` + +### Stopping a Watcher + +A `watch` call returns a stop handle: + +``` js +const stop = watch(...) + +// stop watching +stop() +``` + +If `watch` is called inside lifecycle hooks or `data()` of a component instance, it will automatically be stopped when the associated component instance is unmounted: + +``` js +export default { + created() { + // stopped automatically when the component unmounts + watch(() => this.id, id => { + // ... + }) + } +} +``` + +### Effect Cleanup + +The **effect** callback can also return a cleanup function which gets called every time when: + +- the watcher is about to re-run +- the watcher is stopped + +``` js +watch(idRef, id => { + const token = performAsyncOperation(id) + + return () => { + // id has changed or watcher is stopped. + // invalidate previously pending async operation + token.cancel() + } +}) +``` + +### Non-Immediate Watchers + +To make watchers non-immediate like 2.x, pass additional options via the 3rd argument: + +``` js +watch( + () => count.value + 1, + () => { + console.log(`count changed`) + }, + { immediate: false } +) +``` + +## Exposing Refs to Components + +While this proposal is focused on working with reactive state outside of components, such state should also be usable inside components as well. + +Refs can be returned in a component's `data()` function: + +``` js +import { value } from 'vue' + +export default { + data() { + return { + count: value(0) + } + } +} +``` + +**When a `ref` is returned as a root-level property in `data()`, it is bound to the component instance as a direct property.** This means there's no need to access the value via `.value` - the value can be accessed and mutated directly as `this.count`, and directly as `count` inside templates: + +``` html +
+ {{ count }} +
+``` + +## Beyond the API + +The APIs proposed here are just low-level building blocks. Technically, they provide everything we need for global state management, so Vuex can be rewritten as a very thin layer on top of these APIs. In addition, when combined with [the ability to programmatically hook into the component lifecycle](TODO), we can offer a logic reuse mechanism with capabilities similar to React hooks. + +# Drawbacks + +- To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, these APIs are intended for advanced use cases so the learning cost should be acceptable. + +# Alternatives + +N/A + +# Adoption strategy + +This is mostly new APIs that expose existing internal capabilities. Users familiar with Vue's existing reactivity system should be able to grasp the concept fairly quickly. It should have a dedicated chapter in the official guide, and we also need to revise the [Reactivity in Depth](https://vuejs.org/v2/guide/reactivity.html) section of the current docs. + +# Unresolved questions + +- `watch` API overlaps with existing `this.$watch` API and `watch` component option. In fact, the standalone `watch` API provides a superset of existing APIs. This makes the existence of all three redundant and inconsistent. + + **Should we deprecate `this.$watch` and `watch` component option?** + + Sidenote: removing `this.$watch` and the `watch` option also makes the entire `watch` API completely tree-shakable. + +- We probably need to also expose a `isRef` method to check whether an object is a value/computed ref. From b80def1a39f32f9ad7c089cd1ff2349a3363a158 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 26 Mar 2019 07:57:02 -0400 Subject: [PATCH 2/3] update link to lifecycle rfc --- active-rfcs/0000-advanced-reactivity-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-advanced-reactivity-api.md b/active-rfcs/0000-advanced-reactivity-api.md index f8fda5d3..e3aa2a8e 100644 --- a/active-rfcs/0000-advanced-reactivity-api.md +++ b/active-rfcs/0000-advanced-reactivity-api.md @@ -277,7 +277,7 @@ export default { ## Beyond the API -The APIs proposed here are just low-level building blocks. Technically, they provide everything we need for global state management, so Vuex can be rewritten as a very thin layer on top of these APIs. In addition, when combined with [the ability to programmatically hook into the component lifecycle](TODO), we can offer a logic reuse mechanism with capabilities similar to React hooks. +The APIs proposed here are just low-level building blocks. Technically, they provide everything we need for global state management, so Vuex can be rewritten as a very thin layer on top of these APIs. In addition, when combined with [the ability to programmatically hook into the component lifecycle](https://github.com/vuejs/rfcs/pull/23), we can offer a logic reuse mechanism with capabilities similar to React hooks. # Drawbacks From b1a98331101c84e8ffe3178d674cec54dae9c58e Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 26 Mar 2019 11:34:19 -0400 Subject: [PATCH 3/3] rename refs to pointers to avoid confusion with DOM refs --- active-rfcs/0000-advanced-reactivity-api.md | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/active-rfcs/0000-advanced-reactivity-api.md b/active-rfcs/0000-advanced-reactivity-api.md index e3aa2a8e..d36f38be 100644 --- a/active-rfcs/0000-advanced-reactivity-api.md +++ b/active-rfcs/0000-advanced-reactivity-api.md @@ -21,13 +21,13 @@ watch(() => obj.a, value => { console.log(`obj.a is: ${value}`) }) -// a "ref" object that has a .value property +// a "pointer" object that has a .value property const count = value(0) -// computed "ref" with a read-only .value property +// computed "pointer" with a read-only .value property const plusOne = computed(() => count.value + 1) -// refs can be watched directly +// pointers can be watched directly watch(count, (count, oldCount) => { console.log(`count is: ${count}`) }) @@ -71,7 +71,7 @@ const object = state({ This works exactly like 2.6 `Vue.observable`. The returned object behaves just like a normal object, and when its properties are accessed in **reactive computations** (render functions, computed property getters and watcher getters), they are tracked as dependencies. Mutation to these properties will cause corresponding computations to re-run. -## Value Refs +## Value Pointers The `state` API cannot be used for primitive values because: @@ -84,24 +84,24 @@ The simple solution is wrapping the value in an object wrapper that can be passe ``` js import { value } from 'vue' -const countRef = value(0) +const countPointer = value(0) ``` -The `value` API creates a wrapper object for a value, called a **ref**. A ref is a reactive object with a single property: `.value`. The property points to the actual value being held and is writable: +The `value` API creates a wrapper object for a value, called a **pointer** (This is not technically a C pointer, but the concept is quite close). A pointer is a reactive object with a single property: `.value`. The property points to the actual value being held and is writable: ``` js // read the value -console.log(countRef.value) // 0 +console.log(countPointer.value) // 0 // mutate the value -countRef.value++ +countPointer.value++ ``` -Refs are primarily used for holding primitive values, but it can also hold any other values including deeply nested objects and arrays. Non-primitive values held inside a ref behave like normal reactive objects created via `state`. +Pointers are primarily used for holding primitive values, but it can also hold any other values including deeply nested objects and arrays. Non-primitive values held inside a pointer behave like normal reactive objects created via `state`. -## Computed Refs +## Computed Pointers -In addition to plain value refs, we can also create computed refs: +In addition to plain value pointers, we can also create computed pointers: ``` js import { value, computed } from 'vue' @@ -114,12 +114,12 @@ count.value++ console.log(countPlusOne.value) // 2 ``` -Computed refs are readonly by default - assigning to its `value` property will result in an error. +Computed pointers are readonly by default - assigning to its `value` property will result in an error. -Computed refs can be made writable by passing a write callback as the 2nd argument: +Computed pointers can be made writable by passing a write callback as the 2nd argument: ``` js -const writableRef = computed( +const writablePointer = computed( // read () => count.value + 1, // write @@ -129,7 +129,7 @@ const writableRef = computed( ) ``` -Computed refs behaves like computed properties in a component: it tracks its dependencies and only re-evaluates when dependencies have changed. +Computed pointers behave like computed properties in a component: it tracks its dependencies and only re-evaluates when dependencies have changed. ## Watchers @@ -174,15 +174,15 @@ count.value++ // -> count + 1 is: 2 ``` -### Watching Refs +### Watching Pointers -The 1st argument can also be a ref: +The 1st argument can also be a pointer: ``` js -// double is a computed ref +// double is a computed pointer const double = computed(() => count.value * 2) -// watch a ref directly +// watch a pointer directly watch(double, value => { console.log('double the count is: ', value) }) @@ -224,7 +224,7 @@ The **effect** callback can also return a cleanup function which gets called eve - the watcher is stopped ``` js -watch(idRef, id => { +watch(idPointer, id => { const token = performAsyncOperation(id) return () => { @@ -249,11 +249,11 @@ watch( ) ``` -## Exposing Refs to Components +## Exposing Pointers to Components While this proposal is focused on working with reactive state outside of components, such state should also be usable inside components as well. -Refs can be returned in a component's `data()` function: +Pointers can be returned in a component's `data()` function: ``` js import { value } from 'vue' @@ -267,7 +267,7 @@ export default { } ``` -**When a `ref` is returned as a root-level property in `data()`, it is bound to the component instance as a direct property.** This means there's no need to access the value via `.value` - the value can be accessed and mutated directly as `this.count`, and directly as `count` inside templates: +**When a pointer is returned as a root-level property in `data()`, it is bound to the component instance as a direct property.** This means there's no need to access the value via `.value` - the value can be accessed and mutated directly as `this.count`, and directly as `count` inside templates: ``` html
@@ -281,7 +281,7 @@ The APIs proposed here are just low-level building blocks. Technically, they pro # Drawbacks -- To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, these APIs are intended for advanced use cases so the learning cost should be acceptable. +- To pass state around while keeping them "trackable" and "reactive", values must be passed around in wrapper objects (pointers). This is a new concept and can be a bit more difficult to learn than the base API. However, these APIs are intended for advanced use cases so the learning cost should be acceptable. # Alternatives @@ -299,4 +299,4 @@ This is mostly new APIs that expose existing internal capabilities. Users famili Sidenote: removing `this.$watch` and the `watch` option also makes the entire `watch` API completely tree-shakable. -- We probably need to also expose a `isRef` method to check whether an object is a value/computed ref. +- We probably need to also expose a `isPointer` method to check whether an object is a value/computed pointer.