Skip to content

Commit 91a4425

Browse files
docs: enhance the Reactivity in Depth guide (#934)
* docs: rework the Reactivity in Depth guide * docs: remove the reference to IE 11 compatibility in reactivity.md * docs: add a link explaining VNodes in reactivity.md
1 parent 37ea2b4 commit 91a4425

File tree

1 file changed

+150
-61
lines changed

1 file changed

+150
-61
lines changed

src/guide/reactivity.md

Lines changed: 150 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,72 +13,110 @@ This term comes up in programming quite a bit these days, but what do people mea
1313
Your browser does not support the video tag.
1414
</video>
1515

16-
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.
16+
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.
1717

18-
JavaScript doesn’t usually work like this -- If we were to write something comparable in JavaScript:
18+
JavaScript doesn’t usually work like this. If we were to write something comparable in JavaScript:
1919

2020
```js
21-
var val1 = 2
22-
var val2 = 3
23-
var sum = val1 + val2
21+
let val1 = 2
22+
let val2 = 3
23+
let sum = val1 + val2
2424

25-
// sum
26-
// 5
25+
console.log(sum) // 5
2726

2827
val1 = 3
2928

30-
// sum
31-
// 5
29+
console.log(sum) // Still 5
3230
```
3331

3432
If we update the first value, the sum is not adjusted.
3533

3634
So how would we do this in JavaScript?
3735

38-
- Detect when there’s a change in one of the values
39-
- Track the function that changes it
40-
- Trigger the function so it can update the final value
36+
As a high-level overview, there are a few things we need to be able to do:
4137

42-
## How Vue Tracks These Changes
38+
1. **Track when a value is read.** e.g. `val1 + val2` reads both `val1` and `val2`.
39+
2. **Detect when a value changes.** e.g. When we assign `val1 = 3`.
40+
3. **Re-run the code that read the value originally.** e.g. Run `sum = val1 + val2` again to update the value of `sum`.
4341

44-
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.
42+
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.
4543

46-
<div class="reactivecontent">
47-
<common-codepen-snippet title="Proxies and Vue's Reactivity Explained Visually" slug="VwmxZXJ" tab="result" theme="light" :height="500" :editable="false" :preview="false" />
48-
</div>
44+
First, let's dig a bit deeper into how Vue implements the core reactivity requirements outlined above.
4945

50-
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.**
46+
## How Vue Knows What Code Is Running
5147

52-
We use it like this: `new Proxy(target, handler)`
48+
To be able to run our sum whenever the values change, the first thing we need to do is wrap it in a function:
5349

5450
```js
55-
const dinner = {
56-
meal: 'tacos'
51+
const updateSum = () => {
52+
sum = val1 + val2
5753
}
54+
```
5855

59-
const handler = {
60-
get(target, prop) {
61-
return target[prop]
56+
But how do we tell Vue about this function?
57+
58+
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.
59+
60+
To understand that better, let's try to implement something similar ourselves, without Vue, to see how it might work.
61+
62+
What we need is something that can wrap our sum, like this:
63+
64+
```js
65+
createEffect(() => {
66+
sum = val1 + val2
67+
})
68+
```
69+
70+
We need `createEffect` to keep track of when the sum is running. We might implement it something like this:
71+
72+
```js
73+
// Maintain a stack of running effects
74+
const runningEffects = []
75+
76+
const createEffect = fn => {
77+
// Wrap the passed fn in an effect function
78+
const effect = () => {
79+
runningEffects.push(effect)
80+
fn()
81+
runningEffects.pop()
6282
}
83+
84+
// Automatically run the effect immediately
85+
effect()
6386
}
87+
```
6488

65-
const proxy = new Proxy(dinner, handler)
66-
console.log(proxy.meal)
89+
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.
6790

68-
// tacos
69-
```
91+
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.
92+
93+
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).
7094

71-
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.
95+
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?
96+
97+
## How Vue Tracks These Changes
98+
99+
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.
100+
101+
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.
102+
103+
<div class="reactivecontent">
104+
<common-codepen-snippet title="Proxies and Vue's Reactivity Explained Visually" slug="VwmxZXJ" tab="result" theme="light" :height="500" :editable="false" :preview="false" />
105+
</div>
106+
107+
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.**
108+
109+
We use it like this: `new Proxy(target, handler)`
72110

73111
```js
74112
const dinner = {
75113
meal: 'tacos'
76114
}
77115

78116
const handler = {
79-
get(target, prop) {
117+
get(target, property) {
80118
console.log('intercepted!')
81-
return target[prop]
119+
return target[property]
82120
}
83121
}
84122

@@ -89,17 +127,19 @@ console.log(proxy.meal)
89127
// tacos
90128
```
91129

130+
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.
131+
92132
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.
93133

94-
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:
134+
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:
95135

96136
```js{7}
97137
const dinner = {
98138
meal: 'tacos'
99139
}
100140
101141
const handler = {
102-
get(target, prop, receiver) {
142+
get(target, property, receiver) {
103143
return Reflect.get(...arguments)
104144
}
105145
}
@@ -110,16 +150,16 @@ console.log(proxy.meal)
110150
// tacos
111151
```
112152

113-
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`.
153+
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`:
114154

115155
```js{7}
116156
const dinner = {
117157
meal: 'tacos'
118158
}
119159
120160
const handler = {
121-
get(target, prop, receiver) {
122-
track(target, prop)
161+
get(target, property, receiver) {
162+
track(target, property)
123163
return Reflect.get(...arguments)
124164
}
125165
}
@@ -130,20 +170,22 @@ console.log(proxy.meal)
130170
// tacos
131171
```
132172

133-
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:
173+
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.
174+
175+
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:
134176

135177
```js
136178
const dinner = {
137179
meal: 'tacos'
138180
}
139181

140182
const handler = {
141-
get(target, prop, receiver) {
142-
track(target, prop)
183+
get(target, property, receiver) {
184+
track(target, property)
143185
return Reflect.get(...arguments)
144186
},
145-
set(target, key, value, receiver) {
146-
trigger(target, key)
187+
set(target, property, value, receiver) {
188+
trigger(target, property)
147189
return Reflect.set(...arguments)
148190
}
149191
}
@@ -154,26 +196,66 @@ console.log(proxy.meal)
154196
// tacos
155197
```
156198

157-
Remember this list from a few paragraphs ago? Now we have some answers to how Vue handles these changes:
199+
Remember this list from earlier? Now we have some answers to how Vue implements these key steps:
200+
201+
1. **Track when a value is read**: the `track` function in the proxy's `get` handler records the property and the current effect.
202+
2. **Detect when that value changes**: the `set` handler is called on the proxy.
203+
3. **Re-run the code that read the value originally:** the `trigger` function looks up which effects depend on the property and runs them.
204+
205+
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.
206+
207+
If we were to rewrite our original example using a component we might do it something like this:
208+
209+
```js
210+
const vm = createApp({
211+
data() {
212+
return {
213+
val1: 2,
214+
val2: 3
215+
}
216+
},
217+
computed: {
218+
sum() {
219+
return this.val1 + this.val2
220+
}
221+
}
222+
}).mount('#app')
223+
224+
console.log(vm.sum) // 5
225+
226+
vm.val1 = 3
227+
228+
console.log(vm.sum) // 6
229+
```
230+
231+
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.
158232

159-
- <strike>Detect when there’s a change in one of the values</strike>: we no longer have to do this, as Proxies allow us to intercept it
160-
- **Track the function that changes it**: We do this in a getter within the proxy, called `track`
161-
- **Trigger the function so it can update the final value**: We do in a setter within the proxy, called `trigger`
233+
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.
162234

163-
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.
235+
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:
236+
237+
```js
238+
const proxy = reactive({
239+
val1: 2,
240+
val2: 3
241+
})
242+
```
243+
244+
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.
164245

165246
### Proxied Objects
166247

167248
Vue internally tracks all objects that have been made reactive, so it always returns the same proxy for the same object.
168249

169250
When a nested object is accessed from a reactive proxy, that object is _also_ converted into a proxy before being returned:
170251

171-
```js
252+
```js{6-7}
172253
const handler = {
173-
get(target, prop, receiver) {
174-
track(target, prop)
254+
get(target, property, receiver) {
255+
track(target, property)
175256
const value = Reflect.get(...arguments)
176257
if (isObject(value)) {
258+
// Wrap the nested object in its own reactive proxy
177259
return reactive(value)
178260
} else {
179261
return value
@@ -185,7 +267,7 @@ const handler = {
185267

186268
### Proxy vs. original identity
187269

188-
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:
270+
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:
189271

190272
```js
191273
const obj = {}
@@ -194,29 +276,36 @@ const wrapped = new Proxy(obj, handlers)
194276
console.log(obj === wrapped) // false
195277
```
196278

197-
The original and the wrapped version will behave the same in most cases, but be aware that they will fail
198-
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.
279+
Other operations that rely on strict equality comparisons can also be impacted, such as `.includes()` or `.indexOf()`.
199280

200-
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:
281+
The best practice here is to never hold a reference to the original raw object and only work with the reactive version:
201282

202283
```js
203284
const obj = reactive({
204285
count: 0
205286
}) // no reference to original
206287
```
207288

208-
## Watchers
289+
This ensures that both equality comparisons and reactivity behave as expected.
209290

210-
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.
291+
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:
211292

212-
<div class="reactivecontent">
213-
<common-codepen-snippet title="Second Reactivity with Proxies in Vue 3 Explainer" slug="GRJZddR" tab="result" theme="light" :height="500" :team="false" user="sdras" name="Sarah Drasner" :editable="false" :preview="false" />
214-
</div>
293+
```js
294+
const obj = reactive({
295+
count: 0
296+
})
297+
298+
console.log(obj.count === 0) // true
299+
```
215300

216-
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.
301+
## How Rendering Reacts to Changes
217302

218-
After the first render, a component would have tracked a list of dependencies &mdash; 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.
303+
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.
219304

220-
[//]: # 'TODO: Insert diagram'
305+
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.
306+
307+
<div class="reactivecontent">
308+
<common-codepen-snippet title="Second Reactivity with Proxies in Vue 3 Explainer" slug="GRJZddR" tab="result" theme="light" :height="500" :team="false" user="sdras" name="Sarah Drasner" :editable="false" :preview="false" />
309+
</div>
221310

222311
> 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).

0 commit comments

Comments
 (0)