Skip to content

Commit 2cea411

Browse files
committed
basic reactivity
1 parent 847f12c commit 2cea411

File tree

7 files changed

+338
-145
lines changed

7 files changed

+338
-145
lines changed

src/.vitepress/config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ const sidebar = {
115115
link: '/guide/essentials/template-syntax'
116116
},
117117
{
118-
text: 'State and Reactivity',
119-
link: '/guide/essentials/state-and-reactivity'
118+
text: 'Basic Reactivity',
119+
link: '/guide/essentials/basic-reactivity'
120120
},
121121
{ text: 'Computed Properties', link: '/guide/essentials/computed' },
122122
{ text: 'Watchers and Effects', link: '/guide/essentials/watchers' },

src/.vitepress/theme/components/PreferenceSwitch.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function useToggleFn(
195195
}
196196
197197
.prefer-composition .composition-api {
198-
display: block;
198+
display: initial;
199199
}
200200
201201
.prefer-composition .api-switch .vt-switch-check {
@@ -217,4 +217,10 @@ function useToggleFn(
217217
.prefer-sfc .sfc-switch .vt-switch-check {
218218
transform: translateX(18px);
219219
}
220+
221+
.tip .options-api, .tip .composition-api {
222+
color: var(--vt-c-text-code);
223+
transition: color 0.5s;
224+
font-weight: 600;
225+
}
220226
</style>

src/.vitepress/theme/styles/preferences.css

Whitespace-only changes.

src/guide/advanced/reactivity-in-depth.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,12 @@ A `render` function is conceptually very similar to a `computed` property. Vue d
310310
<!-- <common-codepen-snippet title="Second Reactivity with Proxies in Vue 3 Explainer" slug="wvgqyJK" tab="result" theme="light" :height="500" :editable="false" :preview="false" /> -->
311311
</div>
312312

313-
## Change Detection Caveats in Vue 2
313+
## Reactive Objects with `reactive`
314314

315-
// TODO
315+
### Ref Auto Unwrapping
316+
317+
### Destucturing and Reactivity
318+
319+
### Deep vs. Shallow Reactivity
320+
321+
### Caveats of Proxy-based Reactivity
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
# Basic Reactivity
2+
3+
:::tip API Preference
4+
This section contains different content for Options API and Composition API. Your current preference is <span class="options-api">Options API</span><span class="composition-api">Composition API</span>. You can toggle between the API styles using the "API Preference" switches at the top of the left sidebar.
5+
:::
6+
7+
## Declaring State
8+
9+
<div class="options-api">
10+
11+
With Options API, we use the `data` option to declare reactive state of a component. The option value should be a function that returns an object. Vue will call the function when creating a new component instance, and wrap the returned object in its reactivity system. The wrapped object is stored on the component instance as `$data`. For convenience, any top-level properties of that object are also exposed directly on the component instance (`this` in methods and lifecycle hooks):
12+
13+
```js
14+
export default {
15+
data() {
16+
return {
17+
count: 1
18+
}
19+
},
20+
21+
// `mounted` is a lifecycle hook which we will explain later
22+
mounted() {
23+
// `this` refers to the component instance.
24+
console.log(this.$data.count) // => 1
25+
console.log(this.count) // => 1
26+
27+
// Assigning a value to this.count will also update $data.count
28+
this.count = 2
29+
console.log(this.$data.count) // => 2
30+
31+
// ... and vice-versa
32+
this.$data.count = 3
33+
console.log(this.count) // => 3
34+
}
35+
}
36+
```
37+
38+
These instance properties are only added when the instance is first created, so you need to ensure they are all present in the object returned by the `data` function. Where necessary, use `null`, `undefined` or some other placeholder value for properties where the desired value isn't yet available.
39+
40+
It is possible to add a new property directly to the component instance without including it in `data`. However, because this property isn't backed by the reactive `$data` object, it won't automatically be tracked by [Vue's reactivity system](/guide/advanced/reactivity-in-depth.html).
41+
42+
Vue uses a `$` prefix when exposing its own built-in APIs via the component instance. It also reserves the prefix `_` for internal properties. You should avoid using names for top-level `data` properties that start with either of these characters.
43+
44+
</div>
45+
46+
<div class="composition-api">
47+
48+
### Reactive Variables with `ref`
49+
50+
The primary API for declaring reactive state when using Composition API is the `ref` method:
51+
52+
```js
53+
import { ref } from 'vue'
54+
55+
const count = ref(0)
56+
```
57+
58+
`ref` takes the argument and returns it wrapped within an object with a `value` property, which can then be used to access or mutate the value of the reactive variable:
59+
60+
```js
61+
const count = ref(0)
62+
63+
console.log(count) // { value: 0 }
64+
console.log(count.value) // 0
65+
66+
count.value++
67+
console.log(count.value) // 1
68+
```
69+
70+
There are two reasons why we need to wrap the value in an object:
71+
72+
- **Reactivity tracking**: Vue's reactivity system needs a way to track what state is being used or changed. When you access or mutate a ref via its `.value` property, it triggers the underlying getter/setter functions that notify Vue about what is going on. We discuss more details on how Vue's reactivity system works in [Reactivity in Depth](/guide/advanced/reactivity-in-depth.html).
73+
74+
- **Passing references instead of values**: in JavaScript, primitive types such as strings, numbers and booleans are _passed by value_. This means when you assign a variable to another variable, or pass it to a function, you are passing a copy of the value that is completely disconnected from the original variable:
75+
76+
```js
77+
let count = 0
78+
79+
function log(count) {
80+
setInterval(() => {
81+
// This will always log 0, which is the initial
82+
// value of `count` when log() was called.
83+
console.log(count)
84+
}, 1000)
85+
}
86+
87+
log(count)
88+
count++ // this won't affect the log output.
89+
```
90+
91+
However, objects are always _passed by reference_. So with a ref, we can pass it around while retaining the connection to the original ref:
92+
93+
```js
94+
const count = ref(0)
95+
96+
function log(count) {
97+
setInterval(() => {
98+
// This will always log the latest value.
99+
console.log(count.value)
100+
}, 1000)
101+
}
102+
103+
log(count)
104+
count.value++ // this will affect the log output.
105+
```
106+
107+
This capability is quite important as it unlocks powerful patterns that allow us to compose reactive logic encapsulated in decoupled functions, which we will discuss later in the guide.
108+
109+
### Exposing State to Template
110+
111+
To use refs in a component's template, declare and return them from a component's `setup()` function:
112+
113+
```js
114+
import { ref } from 'vue'
115+
116+
export default {
117+
// `setup` is a special hook dedicated for composition API usage.
118+
setup() {
119+
const count = ref(0)
120+
121+
// expose the ref to the template
122+
return {
123+
count
124+
}
125+
}
126+
}
127+
```
128+
129+
```vue-html
130+
<div>{{ count }}</div>
131+
```
132+
133+
Notice that we don't need `.value` when accessing refs in the template: refs are automatically "unwrapped" when exposed on the template render context.
134+
135+
Manually exposing refs via `setup()` can be verbose. Luckily, it is only necessary when not using a build step. When using Single File Components (SFCs), we can greatly simplify the usage with `<script setup>`:
136+
137+
```vue
138+
<script setup>
139+
import { ref } from 'vue'
140+
141+
const count = ref(0)
142+
</script>
143+
144+
<template>
145+
{{ count }}
146+
</template>
147+
```
148+
149+
Top-level imports and variables declared in `<script setup>` are automatically usable in the template of the same component.
150+
151+
> For the rest of the guide, we will be primarily using SFC + `<script setup>` syntax for Composition API code examples, as that is the most common usage for Vue developers.
152+
153+
</div>
154+
155+
## Declaring Methods
156+
157+
<div class="options-api">
158+
159+
To add methods to a component instance we use the `methods` option. This should be an object containing the desired methods:
160+
161+
```js
162+
const app = Vue.createApp({
163+
data() {
164+
return {
165+
count: 4
166+
}
167+
},
168+
methods: {
169+
increment() {
170+
this.count++
171+
}
172+
},
173+
mounted() {
174+
// methods can be called in lifecycle hooks, or other methods!
175+
this.increment()
176+
}
177+
})
178+
```
179+
180+
Vue automatically binds the `this` value for `methods` so that it always refers to the component instance. This ensures that a method retains the correct `this` value if it's used as an event listener or callback. You should avoid using arrow functions when defining `methods`, as that prevents Vue from binding the appropriate `this` value.
181+
182+
Just like all other properties of the component instance, the `methods` are accessible from within the component's template. Inside a template they are most commonly used as event listeners:
183+
184+
```vue-html
185+
<button @click="increment">Up vote</button>
186+
```
187+
188+
In the example above, the method `increment` will be called when the `<button>` is clicked.
189+
190+
It is also possible to call a method directly from a template. As we'll see shortly, it's usually better to use a [computed property](computed.html) instead. However, using a method can be useful in scenarios where computed properties aren't a viable option. You can call a method anywhere that a template supports JavaScript expressions:
191+
192+
```vue-html
193+
<span :title="toTitleDate(date)">
194+
{{ formatDate(date) }}
195+
</span>
196+
```
197+
198+
If the methods `toTitleDate` or `formatDate` access any reactive data then it will be tracked as a rendering dependency, just as if it had been used in the template directly.
199+
200+
Methods called inside binding expressions should **not** have any side effects, such as changing data or triggering asynchronous operations. If you find yourself tempted to do that you should probably use a [lifecycle hook](/guide/components/lifecycle.html) instead.
201+
202+
### Debouncing and Throttling
203+
204+
Vue doesn't include built-in support for debouncing or throttling but it can be implemented using libraries such as [Lodash](https://lodash.com/).
205+
206+
In cases where a component is only used once, the debouncing can be applied directly within `methods`:
207+
208+
```js
209+
import { debounce } from 'lodash-es'
210+
211+
createApp({
212+
methods: {
213+
// Debouncing with Lodash
214+
click: debounce(function () {
215+
// ... respond to click ...
216+
}, 500)
217+
}
218+
}).mount('#app')
219+
```
220+
221+
:::tip
222+
If you are using the [no-build setup](/guide/quick-start.html#without-build-tools), add the following to your import map: `"lodash-es": "https://cdn.jsdelivr.net/npm/lodash-es/+esm"`
223+
:::
224+
225+
However, this approach is potentially problematic for components that are reused because they'll all share the same debounced function. To keep the component instances independent from each other, we can add the debounced function in the `created` lifecycle hook:
226+
227+
```js
228+
app.component('save-button', {
229+
created() {
230+
// Debouncing with Lodash
231+
this.debouncedClick = _.debounce(this.click, 500)
232+
},
233+
unmounted() {
234+
// Cancel the timer when the component is removed
235+
this.debouncedClick.cancel()
236+
},
237+
methods: {
238+
click() {
239+
// ... respond to click ...
240+
}
241+
},
242+
template: `
243+
<button @click="debouncedClick">
244+
Save
245+
</button>
246+
`
247+
})
248+
```
249+
250+
</div>
251+
252+
<div class="composition-api">
253+
254+
To declare methods when using Composition API, simply declare functions in the same scope with the reactive state:
255+
256+
```js
257+
import { ref } from 'vue'
258+
259+
export default {
260+
setup() {
261+
const count = ref(0)
262+
263+
function increment() {
264+
count.value++
265+
}
266+
267+
// when using manual setup(),
268+
// don't forget to expose the function as well.
269+
return {
270+
count,
271+
increment
272+
}
273+
}
274+
}
275+
```
276+
277+
```html
278+
<button @click="increment">{{ count }}</button>
279+
```
280+
281+
Again, it's much simpler with `<script setup>` as it auto-exposes functions as well:
282+
283+
```vue
284+
<script setup>
285+
import { ref } from 'vue'
286+
287+
const count = ref(0)
288+
289+
function increment() {
290+
count.value++
291+
}
292+
</script>
293+
294+
<template>
295+
<button @click="increment">{{ count }}</button>
296+
</template>
297+
```
298+
299+
</div>
300+
301+
### Ref Transform
302+
303+
The necessity of using `.value` wit refs roots from the language constraints of JavaScript. However, with compile-time transforms we can improve the ergonomics by automatically appending `.value` in appropriate locations. The [ref transform](https://github.com/vuejs/vue-next/tree/master/packages/ref-transform) allows us to write the above example like this:
304+
305+
```vue
306+
<script setup>
307+
let count = $ref(0)
308+
309+
function increment() {
310+
count++
311+
}
312+
</script>
313+
314+
<template>
315+
<button @click="increment">{{ count }}</button>
316+
</template>
317+
```
318+
319+
:::warning Experimental
320+
Ref transform is currently an experimental feature. It is disabled by default and requires explicit opt-in. It may also change before being finalized. More details can be found in its [proposal and discussion on GitHub](https://github.com/vuejs/rfcs/discussions/369).
321+
:::

0 commit comments

Comments
 (0)