|
| 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