From 1f5c25904439bb48b29ac95f898ef34750e9bc85 Mon Sep 17 00:00:00 2001
From: skirtle <65301168+skirtles-code@users.noreply.github.com>
Date: Mon, 8 Mar 2021 20:12:55 +0000
Subject: [PATCH 1/3] docs: rework the Reactivity in Depth guide
---
src/guide/reactivity.md | 211 ++++++++++++++++++++++++++++------------
1 file changed, 150 insertions(+), 61 deletions(-)
diff --git a/src/guide/reactivity.md b/src/guide/reactivity.md
index aab3995cac..67f1310004 100644
--- a/src/guide/reactivity.md
+++ b/src/guide/reactivity.md
@@ -13,62 +13,100 @@ This term comes up in programming quite a bit these days, but what do people mea
Your browser does not support the video tag.
-If you put the number two in the first cell, and the number 3 in the second and asked for the SUM, the spreadsheet would give it to you. No surprises there. But if you update that first number, the SUM automagically updates too.
+If you put the number 2 in the first cell, and the number 3 in the second and asked for the SUM, the spreadsheet would give it to you. No surprises there. But if you update that first number, the SUM automagically updates too.
-JavaScript doesn’t usually work like this -- If we were to write something comparable in JavaScript:
+JavaScript doesn’t usually work like this. If we were to write something comparable in JavaScript:
```js
-var val1 = 2
-var val2 = 3
-var sum = val1 + val2
+let val1 = 2
+let val2 = 3
+let sum = val1 + val2
-// sum
-// 5
+console.log(sum) // 5
val1 = 3
-// sum
-// 5
+console.log(sum) // Still 5
```
If we update the first value, the sum is not adjusted.
So how would we do this in JavaScript?
-- Detect when there’s a change in one of the values
-- Track the function that changes it
-- Trigger the function so it can update the final value
+As a high-level overview, there are a few things we need to be able to do:
-## How Vue Tracks These Changes
+1. **Track when a value is read.** e.g. `val1 + val2` reads both `val1` and `val2`.
+2. **Detect when a value changes.** e.g. When we assign `val1 = 3`.
+3. **Re-run the code that read the value originally.** e.g. Run `sum = val1 + val2` again to update the value of `sum`.
-When you pass a plain JavaScript object to an application or component instance as its `data` option, Vue will walk through all of its properties and convert them to [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) using a handler with getters and setters. This is an ES6-only feature, but we offer a version of Vue 3 that uses the older `Object.defineProperty` to support IE browsers. Both have the same surface API, but the Proxy version is slimmer and offers improved performance.
+We can't do this directly using the code from the previous example but we'll come back to this example later to see how to adapt it to be compatible with Vue's reactivity system.
-
-
-
+First, let's dig a bit deeper into how Vue implements the core reactivity requirements outlined above.
-That was rather quick and requires some knowledge of [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to understand! So let’s dive in a bit. There’s a lot of literature on Proxies, but what you really need to know is that a **Proxy is an object that encases another object or function and allows you to intercept it.**
+## How Vue Knows What Code Is Running
-We use it like this: `new Proxy(target, handler)`
+To be able to run our sum whenever the values change, the first thing we need to do is wrap it in a function:
```js
-const dinner = {
- meal: 'tacos'
+const updateSum = () => {
+ sum = val1 + val2
}
+```
-const handler = {
- get(target, prop) {
- return target[prop]
+But how do we tell Vue about this function?
+
+Vue keeps track of which function is currently running by using an *effect*. An effect is a wrapper around the function that initiates tracking just before the function is called. Vue knows which effect is running at any given point and can run it again when required.
+
+To understand that better, let's try to implement something similar ourselves, without Vue, to see how it might work.
+
+What we need is something that can wrap our sum, like this:
+
+```js
+createEffect(() => {
+ sum = val1 + val2
+})
+```
+
+We need `createEffect` to keep track of when the sum is running. We might implement it something like this:
+
+```js
+// Maintain a stack of running effects
+const runningEffects = []
+
+const createEffect = fn => {
+ // Wrap the passed fn in an effect function
+ const effect = () => {
+ runningEffects.push(effect)
+ fn()
+ runningEffects.pop()
}
+
+ // Automatically run the effect immediately
+ effect()
}
+```
-const proxy = new Proxy(dinner, handler)
-console.log(proxy.meal)
+When our effect is called it pushes itself onto the `runningEffects` array, before calling `fn`. Anything that needs to know which effect is currently running can check that array.
-// tacos
-```
+Effects act as the starting point for many key features. For example, both component rendering and computed properties use effects internally. Any time something magically responds to data changes you can be pretty sure it has been wrapped in an effect.
+
+While Vue's public API doesn't include any way to create an effect directly, it does expose a function called `watchEffect` that behaves a lot like the `createEffect` function from our example. We'll discuss that in more detail [later in the guide](/guide/reactivity-computed-watchers.html#watcheffect).
-Ok, so far, we’re just wrapping that object and returning it. Cool, but not that useful yet. But watch this, we can also intercept this object while we wrap it in the Proxy. This interception is called a trap.
+But knowing what code is running is just one part of the puzzle. How does Vue know what values the effect uses and how does it know when they change?
+
+## How Vue Tracks These Changes
+
+We can't track reassignments of local variables like those in our earlier examples, there's just no mechanism for doing that in JavaScript. What we can track are changes to object properties.
+
+When we return a plain JavaScript object from a component's `data` function, Vue will wrap that object in a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) with handlers for `get` and `set`. This is an ES6-only feature, but we offer a version of Vue 3 that uses the older `Object.defineProperty` to support IE 11 (this is still in development at the time of writing). Both have the same surface API, but the Proxy version is slimmer and offers improved performance.
+
+
+
+
+
+That was rather quick and requires some knowledge of [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to understand! So let’s dive in a bit. There’s a lot of literature on Proxies, but what you really need to know is that a **Proxy is an object that encases another object and allows you to intercept any interactions with that object.**
+
+We use it like this: `new Proxy(target, handler)`
```js
const dinner = {
@@ -76,9 +114,9 @@ const dinner = {
}
const handler = {
- get(target, prop) {
+ get(target, property) {
console.log('intercepted!')
- return target[prop]
+ return target[property]
}
}
@@ -89,9 +127,11 @@ console.log(proxy.meal)
// tacos
```
+Here we've intercepted attempts to read properties of the target object. A handler function like this is also known as a *trap*. There are many different types of trap available, each handling a different type of interaction.
+
Beyond a console log, we could do anything here we wish. We could even _not_ return the real value if we wanted to. This is what makes Proxies so powerful for creating APIs.
-Furthermore, there’s another feature Proxies offer us. Rather than just returning the value like this: `target[prop]`, we could take this a step further and use a feature called `Reflect`, which allows us to do proper `this` binding. It looks like this:
+One challenge with using a Proxy is the `this` binding. We'd like any methods to be bound to the Proxy, rather than the target object, so that we can intercept them too. Thankfully, ES6 introduced another new feature, called `Reflect`, that allows us to make this problem disappear with minimal effort:
```js{7}
const dinner = {
@@ -99,7 +139,7 @@ const dinner = {
}
const handler = {
- get(target, prop, receiver) {
+ get(target, property, receiver) {
return Reflect.get(...arguments)
}
}
@@ -110,7 +150,7 @@ console.log(proxy.meal)
// tacos
```
-We mentioned before that in order to have an API that updates a final value when something changes, we’re going to have to set new values when something changes. We do this in the handler, in a function called `track`, where we pass in the `target` and `key`.
+The first step towards implementing reactivity with a Proxy is to track when a property is read. We do this in the handler, in a function called `track`, where we pass in the `target` and `property`:
```js{7}
const dinner = {
@@ -118,8 +158,8 @@ const dinner = {
}
const handler = {
- get(target, prop, receiver) {
- track(target, prop)
+ get(target, property, receiver) {
+ track(target, property)
return Reflect.get(...arguments)
}
}
@@ -130,7 +170,9 @@ console.log(proxy.meal)
// tacos
```
-Finally, we also set new values when something changes. For this, we’re going to set the changes on our new proxy, by triggering those changes:
+The implementation of `track` isn't shown here. It will check which *effect* is currently running and record that alongside the `target` and `property`. This is how Vue knows that the property is a dependency of the effect.
+
+Finally, we need to re-run the effect when the property value changes. For this we're going to need a `set` handler on our proxy:
```js
const dinner = {
@@ -138,12 +180,12 @@ const dinner = {
}
const handler = {
- get(target, prop, receiver) {
- track(target, prop)
+ get(target, property, receiver) {
+ track(target, property)
return Reflect.get(...arguments)
},
- set(target, key, value, receiver) {
- trigger(target, key)
+ set(target, property, value, receiver) {
+ trigger(target, property)
return Reflect.set(...arguments)
}
}
@@ -154,13 +196,52 @@ console.log(proxy.meal)
// tacos
```
-Remember this list from a few paragraphs ago? Now we have some answers to how Vue handles these changes:
+Remember this list from earlier? Now we have some answers to how Vue implements these key steps:
+
+1. **Track when a value is read**: the `track` function in the proxy's `get` handler records the property and the current effect.
+2. **Detect when that value changes**: the `set` handler is called on the proxy.
+3. **Re-run the code that read the value originally:** the `trigger` function looks up which effects depend on the property and runs them.
+
+The proxied object is invisible to the user, but under the hood it enables Vue to perform dependency-tracking and change-notification when properties are accessed or modified. One caveat is that console logging will format proxied objects differently, so you may want to install [vue-devtools](https://github.com/vuejs/vue-devtools) for a more inspection-friendly interface.
+
+If we were to rewrite our original example using a component we might do it something like this:
+
+```js
+const vm = createApp({
+ data() {
+ return {
+ val1: 2,
+ val2: 3
+ }
+ },
+ computed: {
+ sum() {
+ return this.val1 + this.val2
+ }
+ }
+}).mount('#app')
+
+console.log(vm.sum) // 5
+
+vm.val1 = 3
+
+console.log(vm.sum) // 6
+```
+
+The object returned by `data` will be wrapped in a reactive proxy and stored as `this.$data`. The properties `this.val1` and `this.val2` are aliases for `this.$data.val1` and `this.$data.val2` respectively, so they go through the same proxy.
-- Detect when there’s a change in one of the values: we no longer have to do this, as Proxies allow us to intercept it
-- **Track the function that changes it**: We do this in a getter within the proxy, called `track`
-- **Trigger the function so it can update the final value**: We do in a setter within the proxy, called `trigger`
+Vue will wrap the function for `sum` in an effect. When we try to access `this.sum`, it will run that effect to calculate the value. The reactive proxy around `$data` will track that the properties `val1` and `val2` were read while that effect is running.
-The proxied object is invisible to the user, but under the hood they enable Vue to perform dependency-tracking and change-notification when properties are accessed or modified. As of Vue 3, our reactivity is now available in a [separate package](https://github.com/vuejs/vue-next/tree/master/packages/reactivity). One caveat is that browser consoles format differently when converted data objects are logged, so you may want to install [vue-devtools](https://github.com/vuejs/vue-devtools) for a more inspection-friendly interface.
+As of Vue 3, our reactivity is now available in a [separate package](https://github.com/vuejs/vue-next/tree/master/packages/reactivity). The function that wraps `$data` in a proxy is called [`reactive`](/api/basic-reactivity.html#reactive). We can call this directly ourselves, allowing us to wrap an object in a reactive proxy without needing to use a component:
+
+```js
+const proxy = reactive({
+ val1: 2,
+ val2: 3
+})
+```
+
+We'll explore the functionality exposed by the reactivity package over the course of the next few pages of this guide. That includes functions like `reactive` and `watchEffect` that we've already met, as well as ways to use other reactivity features, such as `computed` and `watch`, without needing to create a component.
### Proxied Objects
@@ -168,12 +249,13 @@ Vue internally tracks all objects that have been made reactive, so it always ret
When a nested object is accessed from a reactive proxy, that object is _also_ converted into a proxy before being returned:
-```js
+```js{6-7}
const handler = {
- get(target, prop, receiver) {
- track(target, prop)
+ get(target, property, receiver) {
+ track(target, property)
const value = Reflect.get(...arguments)
if (isObject(value)) {
+ // Wrap the nested object in its own reactive proxy
return reactive(value)
} else {
return value
@@ -185,7 +267,7 @@ const handler = {
### Proxy vs. original identity
-The use of Proxy does introduce a new caveat to be aware with: the proxied object is not equal to the original object in terms of identity comparison (`===`). For example:
+The use of Proxy does introduce a new caveat to be aware of: the proxied object is not equal to the original object in terms of identity comparison (`===`). For example:
```js
const obj = {}
@@ -194,10 +276,9 @@ const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
```
-The original and the wrapped version will behave the same in most cases, but be aware that they will fail
-operations that rely on strong identity comparisons, such as `.filter()` or `.map()`. This caveat is unlikely to come up when using the options API, because all reactive state is accessed from `this` and guaranteed to already be proxies.
+Other operations that rely on strict equality comparisons can also be impacted, such as `.includes()` or `.indexOf()`.
-However, when using the composition API to explicitly create reactive objects, the best practice is to never hold a reference to the original raw object and only work with the reactive version:
+The best practice here is to never hold a reference to the original raw object and only work with the reactive version:
```js
const obj = reactive({
@@ -205,18 +286,26 @@ const obj = reactive({
}) // no reference to original
```
-## Watchers
+This ensures that both equality comparisons and reactivity behave as expected.
-Every component instance has a corresponding watcher instance, which records any properties "touched" during the component’s render as dependencies. Later on when a dependency’s setter is triggered, it notifies the watcher, which in turn causes the component to re-render.
+Note that Vue does not wrap primitive values such as numbers or strings in a Proxy, so you can still use `===` directly with those values:
-
-
-
+```js
+const obj = reactive({
+ count: 0
+})
+
+console.log(obj.count === 0) // true
+```
-When you pass an object to a component instance as data, Vue converts it to a proxy. This proxy enables Vue to perform dependency-tracking and change-notification when properties are accessed or modified. Each property is considered a dependency.
+## How Rendering Reacts to Changes
-After the first render, a component would have tracked a list of dependencies — the properties it accessed during the render. Conversely, the component becomes a subscriber to each of these properties. When a proxy intercepts a set operation, the property will notify all of its subscribed components to re-render.
+The template for a component is compiled down into a [`render`](/guide/render-function.html) function. The `render` function creates the VNodes that describe how the component should be rendered. It is wrapped in an effect, allowing Vue to track the properties that are 'touched' while it is running.
-[//]: # 'TODO: Insert diagram'
+A `render` function is conceptually very similar to a `computed` property. Vue doesn't track exactly how dependencies are used, it only knows that they were used at some point while the function was running. If any of those properties subsequently changes, it will trigger the effect to run again, re-running the `render` function to generate new VNodes. These are then used to make the necessary changes to the DOM.
+
+
+
+
> If you are using Vue 2.x and below, you may be interested in some of the change detection caveats that exist for those versions, [explored in more detail here](change-detection.md).
From f3bc624abb20451d0ce3a01b99a0816882c9c5ee Mon Sep 17 00:00:00 2001
From: skirtle <65301168+skirtles-code@users.noreply.github.com>
Date: Tue, 9 Mar 2021 23:28:42 +0000
Subject: [PATCH 2/3] docs: remove the reference to IE 11 compatibility in
reactivity.md
---
src/guide/reactivity.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/guide/reactivity.md b/src/guide/reactivity.md
index 67f1310004..221e7cd25b 100644
--- a/src/guide/reactivity.md
+++ b/src/guide/reactivity.md
@@ -98,7 +98,7 @@ But knowing what code is running is just one part of the puzzle. How does Vue kn
We can't track reassignments of local variables like those in our earlier examples, there's just no mechanism for doing that in JavaScript. What we can track are changes to object properties.
-When we return a plain JavaScript object from a component's `data` function, Vue will wrap that object in a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) with handlers for `get` and `set`. This is an ES6-only feature, but we offer a version of Vue 3 that uses the older `Object.defineProperty` to support IE 11 (this is still in development at the time of writing). Both have the same surface API, but the Proxy version is slimmer and offers improved performance.
+When we return a plain JavaScript object from a component's `data` function, Vue will wrap that object in a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) with handlers for `get` and `set`. Proxies were introduced in ES6 and allow Vue 3 to avoid some of the reactivity caveats that existed in earlier versions of Vue.
From 0978d748ae0c2a0413518fb1ad243347e96025ca Mon Sep 17 00:00:00 2001
From: skirtle <65301168+skirtles-code@users.noreply.github.com>
Date: Tue, 9 Mar 2021 23:30:13 +0000
Subject: [PATCH 3/3] docs: add a link explaining VNodes in reactivity.md
---
src/guide/reactivity.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/guide/reactivity.md b/src/guide/reactivity.md
index 221e7cd25b..4f686402e0 100644
--- a/src/guide/reactivity.md
+++ b/src/guide/reactivity.md
@@ -300,7 +300,7 @@ console.log(obj.count === 0) // true
## How Rendering Reacts to Changes
-The template for a component is compiled down into a [`render`](/guide/render-function.html) function. The `render` function creates the VNodes that describe how the component should be rendered. It is wrapped in an effect, allowing Vue to track the properties that are 'touched' while it is running.
+The template for a component is compiled down into a [`render`](/guide/render-function.html) function. The `render` function creates the [VNodes](/guide/render-function.html#the-virtual-dom-tree) that describe how the component should be rendered. It is wrapped in an effect, allowing Vue to track the properties that are 'touched' while it is running.
A `render` function is conceptually very similar to a `computed` property. Vue doesn't track exactly how dependencies are used, it only knows that they were used at some point while the function was running. If any of those properties subsequently changes, it will trigger the effect to run again, re-running the `render` function to generate new VNodes. These are then used to make the necessary changes to the DOM.