Computed Properties and Watchers
Computed Properties
in-template expression 很方便好用,但不適合寫太多 code logic
<div id="computed-basics">
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>
</div>
<script type="text/javascript">
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
}
}).mount('#computed-basics')
</script>
為了簡化上面的 code,要使用 computed properties
<div id="computed-basics2">
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</div>
<script type="text/javascript">
Vue.createApp({
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// a computed getter
publishedBooksMessage() {
// `this` points to the vm instance
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}).mount('#computed-basics2')
</script>
因為 vm.publishedBooksMessage 相依於 vm.author.books,如果修改了 books,publishedBooksMessage 會自動更新
- Computed Caching vs Methods
剛剛的 sample,也可以用 method 呼叫改寫
<p>{{ calculateBooksMessage() }}</p>
// in component
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
兩種寫法的結果一樣
差異是,computed properties 會根據 reactive dependencies 被 cached。
computed property 只會在 reactive dependencies 被修改時,重新 re-evaluate。
只要 author.books 沒有被修改,使用 publishedBooksMessage 都會直接 return,而不會運算該 function
因為 Date.now() 不是 reactive dependency,所以 now 這個 computed property 永遠不會更新
computed: {
now() {
return Date.now()
}
}
如果是 method,就會在 re-render 時,重複呼叫該 function
如果不需要 caching,就用 method 寫法
- Computed Setter
computed properties 預設為 getter-only,如果需要時,可增加 setter
// ...
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...
當呼叫 vm.fullName = 'John'
時,會呼叫 setter,並更新 vm.firstName, vm.lastName
Watchers
vue 提供 watch
option 可以 react to data change
如果修改 data 會發生 asynchronous or expensive operations 時很有用
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</div>
<!-- Since there is already a rich ecosystem of ajax libraries -->
<!-- and collections of general-purpose utility methods, Vue core -->
<!-- is able to remain small by not reinventing them. This also -->
<!-- gives you the freedom to use what you're familiar with. -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script>
const watchExampleVM = Vue.createApp({
data() {
return {
question: '',
answer: 'Questions usually contain a question mark. ;-)'
}
},
watch: {
// whenever question changes, this function will run
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...'
axios
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
})
.catch(error => {
this.answer = 'Error! Could not reach the API. ' + error
})
}
}
}).mount('#watch-example')
</script>
- computed vs wated property
當有一些 data,需要根據其他 data 動態改變時,可能會 overuse watch
,尤其是有 AngularJS 背景的開發者,要先考慮使用 computed property 而不是 watch callback
<div id="demo">{{ fullName }}</div>
<script>
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
},
watch: {
firstName(val) {
this.fullName = val + ' ' + this.lastName
},
lastName(val) {
this.fullName = this.firstName + ' ' + val
}
}
}).mount('#demo')
</script>
watch 的版本會比較精簡
const vm = Vue.createApp({
data() {
return {
firstName: 'Foo',
lastName: 'Bar'
}
},
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
}).mount('#demo')
Class & Style Bindings
另一個常見需求是修改 element 的 class list 與 inline styles,因為都是 attributes,可以用 v-bind
修改。但會遇到很多 string 連接的問題。
vue 提供 class
與 sytle
,以 object/array 方式處理
Binding html classes
- Object Syntax
可傳送物件到 :class
這是 v-bind:class
的縮寫
ex: 根據 data property: isActive
的 Truthy 決定 active
class
<div :class="{ active: isActive }"></div>
ex: 可以有多個欄位,也可跟既有的 class 並存
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>
以下的物件
data() {
return {
isActive: true,
hasError: false
}
}
會 render 為
<div class="static active"></div>
ex: 可以包裝為一個物件
<div :class="classObject"></div>
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}
ex: 可以使用 computed property
<div :class="classObject"></div>
data() {
return {
isActive: true,
error: null
}
},
computed: {
classObject() {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
- Array Syntax
可用 array 傳入 :class
<div :class="[activeClass, errorClass]"></div>
data() {
return {
activeClass: 'active',
errorClass: 'text-danger'
}
}
會 render 為
<div class="active text-danger"></div>
ex: 可以用 ternary expression 處理 class toggle
<div :class="[isActive ? activeClass : '', errorClass]"></div>
ex: 如果有多個 conditional class,可以在 array 裡面用 object syntax
<div :class="[{ active: isActive }, errorClass]"></div>
- with Components
在單一 root component 使用 class
時,既有的 classes 不會被 overwritten
<div id="app">
<my-component class="baz boo"></my-component>
</div>
<script>
const app = Vue.createApp({})
app.component('my-component', {
template: `<p class="foo bar">Hi!</p>`
})
</script>
會 render 為
<p class="foo bar baz boo">Hi</p>
如果是多個 root element,必須指定哪一個 component 接收這些 class,可使用 $attrs
component property
<div id="app">
<my-component class="baz"></my-component>
</div>
<script>
const app = Vue.createApp({})
app.component('my-component', {
template: `
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
`
})
</script>
Binding inline styles
- Object Syntax
可使用 camelCase or kebab-case (use quotes with kebab-case) 的 css propery names
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<script>
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
</script>
比較好的寫法是 bind sytle object
<div :style="styleObject"></div>
<script>
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
</script>
- Array Syntax
<div :style="[baseStyles, overridingStyles]"></div>
- Auto-prefixing
如果在 :style
使用需要 Vendor Prefix 的 css property,vue 會自動加上適當的 prefix,Vue 會在 runtime 自動判斷是否有被目前的 browser 支援。
如果沒有支援,就會自動測試多個 prefix variants,嘗試找到支援的 css property
- Multiple Values
可提供多個 prefixed values 給某個 style property
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
Conditional Rendering
v-if
在 directive expression 回傳 truth value 時,才會被 rendered
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
- 在 使用 conditional groups
如果要 toggle 多個 elements,可在
<template>
使用v-if
,<template>
是一個 invisible wrapper,最後 render 的結果不會出現<template>
<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template>
- v-else
可用
v-else
代表 else block<div v-if="Math.random() > 0.5"> Now you see me </div> <div v-else> Now you don't </div>
- v-else-if
<div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div>
v-show
v-show
的 element 一定會出現在 DOM,但利用display
css property 決定要不要顯示<h1 v-show="ok">Hello!</h1>
v-show
不支援<template>
,也不支援v-else
v-if vs v-show
v-if
會 destroy & re-create element,如果一開始條件為 false,就不會被 renderedv-show
會先 render,再用 css toggling如果要常常 toggling,就用
v-show
如果條件在 runtime 不會改變,就用
v-if
v-if with v-for
不建議
v-if
v-for
併用如果在同一個 element 出現
v-if
v-for
,會先運算v-if
List Rendering
用 v-for 將 array 對應到 elements
用特殊的
item in items
語法,使用 items array 裡面的每一個元素<ul id="array-rendering"> <li v-for="item in items"> {{ item.message }} </li> </ul> <script> Vue.createApp({ data() { return { items: [{ message: 'Foo' }, { message: 'Bar' }] } } }).mount('#array-rendering') </script>
v-for
block 可存取 parent scope properties,且支援 optional second argument: index<ul id="array-with-index"> <li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li> </ul> <script> Vue.createApp({ data() { return { parentMessage: 'Parent', items: [{ message: 'Foo' }, { message: 'Bar' }] } } }).mount('#array-with-index') </script>
會 render 為
• Parent - 0 - Foo • Parent - 1 - Bar
也可以寫成
item of items
<div v-for="item of items"></div>
v-for with an Object
可 iterate properties of an object
<ul id="v-for-object" class="demo"> <li v-for="value in myObject"> {{ value }} </li> </ul> <script> Vue.createApp({ data() { return { myObject: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } } }).mount('#v-for-object') </script>
會 render 為
• How to do lists in Vue • Jane Doe • 2016-04-10
可使用 propery name
<li v-for="(value, name) in myObject"> {{ name }}: {{ value }} </li>
使用 index
<li v-for="(value, name, index) in myObject"> {{ index }}. {{ name }}: {{ value }} </li>
Maintaining State
當
v-for
更新 list of elements 時,會使用in-place path
策略vue 會逐項更新每一個 index 的資料
但這個方法只適用於 output 不會被 child component state 或 temporary DOM state (ex: form input value) 影響的時候
為了讓 vue 能夠識別每一個 node,並 reuse, reorder 既有的 elements,要為每一個 item 提供唯一的
key
attribute<div v-for="item in items" :key="item.id"> <!-- content --> </div>
注意:不要用 non-primitive values (ex: objects, arrays) 作為 keys,要使用 string 或 numeric values
Array Change Detection
- mutation methods
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
<ul id="array-with-index"> <li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li> </ul> <script> const vm = Vue.createApp({ data() { return { parentMessage: 'Parent', items: [{ message: 'Foo' }, { message: 'Bar' }] } }, methods: { update() { this.items[1] = { message: 'BarUpdated' }; this.items.push({ message: 'Que' }); } } }).mount('#array-with-index') </script>
在 console 呼叫
vm.update();
也可以在 console 測試
vm.items[1] = { message: 'BarUpdated' }; vm.items.push({ message: 'Que' });
- replacing an array
filter()
concat()
slice()
不會直接更新 array,會直接回傳新的 array,故必需要指定給原本的 data property,更新整個 array// 頁面沒有更新 vm.items.filter(item => item.message.match(/Foo/)) // vm.items = vm.items.filter(item => item.message.match(/Foo/))
Displaying Filtered/Sorted Results
有時候會需要顯示 filtered/sorted version of an array,但不更動原始的 data。
可利用 computed property
<ul id="array-with-index"> <li v-for="n in evenNumbers" :key="n">{{ n }}</li> </ul> <script> const vm = Vue.createApp({ data() { return { numbers: [ 1, 2, 3, 4, 5 ] } }, computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) } } }).mount('#array-with-index') </script>
如果遇到不能用 computed properties 時 (ex: nested v-for loop),可改用 method
<div id="test"> <ul v-for="numbers in sets"> <li v-for="n in even(numbers)" :key="n">{{ n }}</li> </ul> </div> <script> const vm = Vue.createApp({ data() { return { sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]] } }, methods: { even(numbers) { return numbers.filter(number => number % 2 === 0) } } }).mount('#test') </script>
頁面結果
• 2 • 4 • 6 • 8 • 10
v-for with a Range
v-for
可使用 integer,用來代表要 repeat template 的次數<div id="range" class="demo"> <span v-for="n in 10" :key="n">{{ n }} </span> </div> <script> Vue.createApp({}).mount('#range') </script>
頁面結果
12345678910
v-for on a
<template>
類似
v-if
<ul> <template v-for="item in items" :key="item.msg"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul>
v-for with v-if
不建議
v-for
跟v-if
併用v-if
的 priority 比較高。v-if
無法使用v-for
scope 的變數<!-- This will throw an error because property "todo" is not defined on instance. --> <li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo.name }} </li>
用
<template>
解決<template v-for="todo in todos" :key="todo.name"> <li v-if="!todo.isComplete"> {{ todo.name }} </li> </template>
v-for with a Component
可直接把
v-for
用在 custom component<my-component v-for="item in items" :key="item.id"></my-component>
但這樣寫,無法將 data 傳入 component,因為 scope 不同
要用 props 改寫
<my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id" ></my-component>
完整的 todo list sample
<div id="todo-list-example"> <form v-on:submit.prevent="addNewTodo"> <label for="new-todo">Add a todo</label> <input v-model="newTodoText" id="new-todo" placeholder="E.g. Feed the cat" /> <button>Add</button> </form> <ul> <todo-item v-for="(todo, index) in todos" :key="todo.id" :title="todo.title" @remove="todos.splice(index, 1)" ></todo-item> </ul> </div> <script> const app = Vue.createApp({ data() { return { newTodoText: '', todos: [ { id: 1, title: 'Do the dishes' }, { id: 2, title: 'Take out the trash' }, { id: 3, title: 'Mow the lawn' } ], nextTodoId: 4 } }, methods: { addNewTodo() { this.todos.push({ id: this.nextTodoId++, title: this.newTodoText }) this.newTodoText = '' } } }) app.component('todo-item', { template: ` <li> {{ title }} <button @click="$emit('remove')">Remove</button> </li> `, props: ['title'], emits: ['remove'] }) app.mount('#todo-list-example') </script>
References
沒有留言:
張貼留言