2022/03/14

Vue.js Essentials 2

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 提供 classsytle,以 object/array 方式處理

Binding html classes

  • Object Syntax

可傳送物件到 :class 這是 v-bind:class 的縮寫

ex: 根據 data property: isActiveTruthy 決定 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>

沒有留言:

張貼留言