Skip to content

Commit 31ce814

Browse files
author
ntepluhina
committed
feat: updated computed properties
1 parent 7abdd5a commit 31ce814

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<template>
2+
<div id="example" class="demo">
3+
<p>Original message: "{{ message }}"</p>
4+
<p>Computed reversed message: "{{ reversedMessage }}"</p>
5+
</div>
6+
</template>
7+
8+
<script>
9+
export default {
10+
data() {
11+
return {
12+
message: 'Hello'
13+
}
14+
},
15+
computed: {
16+
reversedMessage: function () {
17+
return this.message.split('').reverse().join('')
18+
}
19+
}
20+
}
21+
</script>

src/guide/computed.md

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# Computed Properties and Watchers
2+
3+
## Computed Properties
4+
5+
<div class="vueschool"><a href="https://vueschool.io/lessons/vuejs-computed-properties?friend=vuejs" target="_blank" rel="sponsored noopener" title="Learn how computed properties work with Vue School">Learn how computed properties work with a free lesson on Vue School</a></div>
6+
7+
In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain. For example:
8+
9+
```html
10+
<div id="example">
11+
{{ message.split('').reverse().join('') }}
12+
</div>
13+
```
14+
15+
At this point, the template is no longer simple and declarative. You have to look at it for a second before realizing that it displays `message` in reverse. The problem is made worse when you want to include the reversed message in your template more than once.
16+
17+
That's why for any complex logic, you should use a **computed property**.
18+
19+
### Basic Example
20+
21+
```html
22+
<div id="example">
23+
<p>Original message: "{{ message }}"</p>
24+
<p>Computed reversed message: "{{ reversedMessage }}"</p>
25+
</div>
26+
```
27+
28+
```js
29+
const vm = Vue.createApp().mount(
30+
{
31+
data: {
32+
message: "Hello"
33+
},
34+
computed: {
35+
// a computed getter
36+
reversedMessage() {
37+
// `this` points to the vm instance
38+
return this.message.split("").reverse().join("")
39+
}
40+
}
41+
},
42+
"#example"
43+
);
44+
```
45+
46+
Result:
47+
48+
<computed-1/>
49+
50+
Here we have declared a computed property `reversedMessage`. The function we provided will be used as the getter function for the property `vm.reversedMessage`:
51+
52+
```js
53+
console.log(vm.reversedMessage); // => 'olleH'
54+
vm.message = "Goodbye";
55+
console.log(vm.reversedMessage); // => 'eybdooG'
56+
```
57+
58+
You can open the sandbox(TODO) and play with the example vm yourself. The value of `vm.reversedMessage` is always dependent on the value of `vm.message`.
59+
60+
You can data-bind to computed properties in templates just like a normal property. Vue is aware that `vm.reversedMessage` depends on `vm.message`, so it will update any bindings that depend on `vm.reversedMessage` when `vm.message` changes. And the best part is that we've created this dependency relationship declaratively: the computed getter function has no side effects, which makes it easier to test and understand.
61+
62+
### Computed Caching vs Methods
63+
64+
You may have noticed we can achieve the same result by invoking a method in the expression:
65+
66+
```html
67+
<p>Reversed message: "{{ reverseMessage() }}"</p>
68+
```
69+
70+
```js
71+
// in component
72+
methods: {
73+
reverseMessage() {
74+
return this.message.split('').reverse().join('')
75+
}
76+
}
77+
```
78+
79+
Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that **computed properties are cached based on their reactive dependencies.** A computed property will only re-evaluate when some of its reactive dependencies have changed. This means as long as `message` has not changed, multiple access to the `reversedMessage` computed property will immediately return the previously computed result without having to run the function again.
80+
81+
This also means the following computed property will never update, because `Date.now()` is not a reactive dependency:
82+
83+
```js
84+
computed: {
85+
now() {
86+
return Date.now()
87+
}
88+
}
89+
```
90+
91+
In comparison, a method invocation will **always** run the function whenever a re-render happens.
92+
93+
Why do we need caching? Imagine we have an expensive computed property **A**, which requires looping through a huge array and doing a lot of computations. Then we may have other computed properties that in turn depend on **A**. Without caching, we would be executing **A**’s getter many more times than necessary! In cases where you do not want caching, use a method instead.
94+
95+
### Computed vs Watched Property
96+
97+
Vue does provide a more generic way to observe and react to data changes on a Vue instance: **watch properties**. When you have some data that needs to change based on some other data, it is tempting to overuse `watch` - especially if you are coming from an AngularJS background. However, it is often a better idea to use a computed property rather than an imperative `watch` callback. Consider this example:
98+
99+
```html
100+
<div id="demo">{{ fullName }}</div>
101+
```
102+
103+
```js
104+
const vm = Vue.createApp().mount({
105+
data: {
106+
firstName: "Foo",
107+
lastName: "Bar",
108+
fullName: "Foo Bar"
109+
},
110+
watch: {
111+
firstName(val) {
112+
this.fullName = val + " " + this.lastName;
113+
},
114+
lastName(val) {
115+
this.fullName = this.firstName + " " + val;
116+
}
117+
}
118+
}, '#demo');
119+
```
120+
121+
The above code is imperative and repetitive. Compare it with a computed property version:
122+
123+
```js
124+
const vm = Vue.createApp().mount({
125+
data: {
126+
firstName: "Foo",
127+
lastName: "Bar"
128+
},
129+
computed: {
130+
fullName() {
131+
return this.firstName + " " + this.lastName;
132+
}
133+
}
134+
}, '#demo');
135+
```
136+
137+
Much better, isn't it?
138+
139+
### Computed Setter
140+
141+
Computed properties are by default getter-only, but you can also provide a setter when you need it:
142+
143+
```js
144+
// ...
145+
computed: {
146+
fullName: {
147+
// getter
148+
get() {
149+
return this.firstName + ' ' + this.lastName
150+
},
151+
// setter
152+
set(newValue) {
153+
const names = newValue.split(' ')
154+
this.firstName = names[0]
155+
this.lastName = names[names.length - 1]
156+
}
157+
}
158+
}
159+
// ...
160+
```
161+
162+
Now when you run `vm.fullName = 'John Doe'`, the setter will be invoked and `vm.firstName` and `vm.lastName` will be updated accordingly.
163+
164+
## Watchers
165+
166+
While computed properties are more appropriate in most cases, there are times when a custom watcher is necessary. That's why Vue provides a more generic way to react to data changes through the `watch` option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data.
167+
168+
For example:
169+
170+
```html
171+
<div id="watch-example">
172+
<p>
173+
Ask a yes/no question:
174+
<input v-model="question" />
175+
</p>
176+
<p>{{ answer }}</p>
177+
</div>
178+
```
179+
180+
```html
181+
<!-- Since there is already a rich ecosystem of ajax libraries -->
182+
<!-- and collections of general-purpose utility methods, Vue core -->
183+
<!-- is able to remain small by not reinventing them. This also -->
184+
<!-- gives you the freedom to use what you're familiar with. -->
185+
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
186+
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
187+
<script>
188+
var watchExampleVM = new Vue({
189+
el: "#watch-example",
190+
data: {
191+
question: "",
192+
answer: "I cannot give you an answer until you ask a question!"
193+
},
194+
watch: {
195+
// whenever question changes, this function will run
196+
question: function(newQuestion, oldQuestion) {
197+
this.answer = "Waiting for you to stop typing...";
198+
this.debouncedGetAnswer();
199+
}
200+
},
201+
created: function() {
202+
// _.debounce is a function provided by lodash to limit how
203+
// often a particularly expensive operation can be run.
204+
// In this case, we want to limit how often we access
205+
// yesno.wtf/api, waiting until the user has completely
206+
// finished typing before making the ajax request. To learn
207+
// more about the _.debounce function (and its cousin
208+
// _.throttle), visit: https://lodash.com/docs#debounce
209+
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500);
210+
},
211+
methods: {
212+
getAnswer: function() {
213+
if (this.question.indexOf("?") === -1) {
214+
this.answer = "Questions usually contain a question mark. ;-)";
215+
return;
216+
}
217+
this.answer = "Thinking...";
218+
var vm = this;
219+
axios
220+
.get("https://yesno.wtf/api")
221+
.then(function(response) {
222+
vm.answer = _.capitalize(response.data.answer);
223+
})
224+
.catch(function(error) {
225+
vm.answer = "Error! Could not reach the API. " + error;
226+
});
227+
}
228+
}
229+
});
230+
</script>
231+
```
232+
233+
Result:
234+
235+
{% raw %}
236+
237+
<div id="watch-example" class="demo">
238+
<p>
239+
Ask a yes/no question:
240+
<input v-model="question">
241+
</p>
242+
<p>{{ answer }}</p>
243+
</div>
244+
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
245+
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
246+
<script>
247+
var watchExampleVM = new Vue({
248+
el: '#watch-example',
249+
data: {
250+
question: '',
251+
answer: 'I cannot give you an answer until you ask a question!'
252+
},
253+
watch: {
254+
question: function (newQuestion, oldQuestion) {
255+
this.answer = 'Waiting for you to stop typing...'
256+
this.debouncedGetAnswer()
257+
}
258+
},
259+
created: function () {
260+
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
261+
},
262+
methods: {
263+
getAnswer: function () {
264+
if (this.question.indexOf('?') === -1) {
265+
this.answer = 'Questions usually contain a question mark. ;-)'
266+
return
267+
}
268+
this.answer = 'Thinking...'
269+
var vm = this
270+
axios.get('https://yesno.wtf/api')
271+
.then(function (response) {
272+
vm.answer = _.capitalize(response.data.answer)
273+
})
274+
.catch(function (error) {
275+
vm.answer = 'Error! Could not reach the API. ' + error
276+
})
277+
}
278+
}
279+
})
280+
</script>
281+
{% endraw %}
282+
283+
In this case, using the `watch` option allows us to perform an asynchronous operation (accessing an API), limit how often we perform that operation, and set intermediary states until we get a final answer. None of that would be possible with a computed property.
284+
285+
In addition to the `watch` option, you can also use the imperative [vm.\$watch API](../api/#vm-watch).

0 commit comments

Comments
 (0)