1. 程式人生 > >Vue.js基礎學習(二)

Vue.js基礎學習(二)

Vue.js基礎學習 二

Vue 例項

1.建立一個 Vue 例項

每個 Vue 應用都是通過用 Vue 函式建立一個新的 Vue 例項開始的:

var vm = new Vue({
  // 選項
})

雖然沒有完全遵循 MVVM 模型,但是 Vue 的設計也受到了它的啟發。因此在文件中經常會使用 vm (ViewModel 的縮寫) 這個變數名錶示 Vue 例項。
當建立一個 Vue 例項時,你可以傳入一個選項物件。這篇教程主要描述的就是如何使用這些選項來建立你想要的行為。作為參考。

一個 Vue 應用由一個通過 new Vue 建立的根 Vue 例項,以及可選的巢狀的、可複用的元件樹組成。舉個例子,一個 todo 應用的元件樹可以是這樣的:

根例項
└─ TodoList
├─ TodoItem
│ ├─ DeleteTodoButton
│ └─ EditTodoButton
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
我們會在稍後的元件系統章節具體展開。不過現在,你只需要明白所有的 Vue 元件都是 Vue 例項,並且接受相同的選項物件 (一些根例項特有的選項除外)。

2.資料與方法

當一個 Vue 例項被建立時,它向 Vue 的響應式系統中加入了其 data 物件中能找到的所有的屬性。當這些屬性的值發生改變時,檢視將會產生“響應”,即匹配更新為新的值。

// 我們的資料物件
var data = { a: 1 }

// 該物件被加入到一個 Vue 例項中
var vm = new Vue({
  data: data
})

// 獲得這個例項上的屬性
// 返回源資料中對應的欄位
vm.a == data.a // => true

// 設定屬性也會影響到原始資料
vm.a = 2
data.a // => 2

// ……反之亦然
data.a = 3
vm.a // => 3

當這些資料改變時,檢視會進行重渲染。值得注意的是只有當例項被建立時 data 中存在的屬性才是響應式的。也就是說如果你新增一個新的屬性,比如:

vm.b = 'hi'

那麼對 b 的改動將不會觸發任何檢視的更新。如果你知道你會在晚些時候需要一個屬性,但是一開始它為空或不存在,那麼你僅需要設定一些初始值。比如:

data: {
  newTodoText: '',
  visitCount: 0,
  hideCompletedTodos: false,
  todos: [],
  error: null
}

這裡唯一的例外是使用 Object.freeze(),這會阻止修改現有的屬性,也意味著響應系統無法再追蹤變化。

var obj = {
  foo: 'bar'
}

Object.freeze(obj)

new Vue({
  el: '#app',
  data: obj
})
<div id="app">
  <p>{{ foo }}</p>
  <!-- 這裡的 `foo` 不會更新! -->
  <button v-on:click="foo = 'baz'">Change it</button>
</div>

除了資料屬性,Vue 例項還暴露了一些有用的例項屬性與方法。它們都有字首 $,以便與使用者定義的屬性區分開來。例如:

var data = { a: 1 }
var vm = new Vue({
  el: '#example',
  data: data
})

vm.$data === data // => true
vm.$el === document.getElementById('example') // => true

// $watch 是一個例項方法
vm.$watch('a', function (newValue, oldValue) {
  // 這個回撥將在 `vm.a` 改變後呼叫
})

3.例項生命週期鉤子

每個 Vue 例項在被建立時都要經過一系列的初始化過程——例如,需要設定資料監聽、編譯模板、將例項掛載到 DOM 並在資料變化時更新 DOM 等。同時在這個過程中也會執行一些叫做生命週期鉤子的函式,這給了使用者在不同階段新增自己的程式碼的機會。

比如created 鉤子可以用來在一個例項被建立之後執行程式碼:

new Vue({
  data: {
    a: 1
  },
  created: function () {
    // `this` 指向 vm 例項
    console.log('a is: ' + this.a)
  }
})
// => "a is: 1"

也有一些其它的鉤子,在例項生命週期的不同階段被呼叫,如 mountedupdateddestroyed。生命週期鉤子的this 上下文指向呼叫它的 Vue 例項。

不要在選項屬性或回撥上使用箭頭函式,比如 created: () => console.log(this.a) 或 vm.$watch('a', newValue => this.myMethod())。因為箭頭函式是和父級上下文繫結在一起的,this 不會是如你所預期的 Vue 例項,經常導致 Uncaught TypeError: Cannot read property of undefined 或 Uncaught TypeError: this.myMethod is not a function 之類的錯誤。

4.生命週期圖示

下圖展示了例項的生命週期。你不需要立馬弄明白所有的東西,不過隨著你的不斷學習和使用,它的參考價值會越來越高。
在這裡插入圖片描述

模板語法

Vue.js 使用了基於 HTML 的模板語法,允許開發者宣告式地將 DOM 繫結至底層 Vue 例項的資料。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循規範的瀏覽器和 HTML 解析器解析。

在底層的實現上,Vue 將模板編譯成虛擬 DOM 渲染函式。結合響應系統,Vue 能夠智慧地計算出最少需要重新渲染多少元件,並把 DOM 操作次數減到最少。

如果你熟悉虛擬 DOM 並且偏愛 JavaScript 的原始力量,你也可以不用模板,直接寫渲染 (render) 函式,使用可選的 JSX 語法。

1.插值

文字(v-once 指令:執行一次性地插值)

資料繫結最常見的形式就是使用“Mustache”語法 (雙大括號) 的文字插值:

<span>Message: {{ msg }}</span>

Mustache 標籤將會被替代為對應資料物件上 msg 屬性的值。無論何時,繫結的資料物件上msg屬性發生了改變,插值處的內容都會更新。

通過使用v-once指令,你也能執行一次性地插值,當資料改變時,插值處的內容不會更新。但請留心這會影響到該節點上的其它資料繫結:

<span v-once>這個將不會改變: {{ msg }}</span>

原始 HTML(v-html 來複合局部模板)

雙大括號會將資料解釋為普通文字,而非 HTML 程式碼。為了輸出真正的 HTML,你需要使用 v-html 指令:

<p>Using mustaches: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

這個 span的內容將會被替換成為屬性值 rawHtml,直接作為 HTML——會忽略解析屬性值中的資料繫結。注意,你不能使用 v-html 來複合局部模板,因為 Vue 不是基於字串的模板引擎。反之,對於使用者介面 (UI),元件更適合作為可重用和可組合的基本單位。

你的站點上動態渲染的任意 HTML 可能會非常危險,因為它很容易導致 XSS 攻擊。請只對可信內容使用 HTML 插值,絕不要對使用者提供的內容使用插值。

特性

Mustache 語法不能作用在 HTML 特性上,遇到這種情況應該使用 v-bind指令:

<div v-bind:id="dynamicId"></div>

在布林特性的情況下,它們的存在即暗示為 truev-bind 工作起來略有不同,在這個例子中:

<button v-bind:disabled="isButtonDisabled">Button</button>

如果isButtonDisabled的值是 nullundefinedfalse,則 disabled 特性甚至不會被包含在渲染出來的<button> 元素中。

使用 JavaScript 表示式

迄今為止,在我們的模板中,我們一直都只繫結簡單的屬性鍵值。但實際上,對於所有的資料繫結,Vue.js 都提供了完全的 JavaScript 表示式支援。

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

這些表示式會在所屬 Vue 例項的資料作用域下作為 JavaScript 被解析。有個限制就是,每個繫結都只能包含單個表示式,所以下面的例子都不會生效。

<!-- 這是語句,不是表示式 -->
{{ var a = 1 }}

<!-- 流控制也不會生效,請使用三元表示式 -->
{{ if (ok) { return message } }}

模板表示式都被放在沙盒中,只能訪問全域性變數的一個白名單,如MathDate。你不應該在模板表示式中試圖訪問使用者定義的全域性變數。

2.指令

指令 (Directives) 是帶有v-字首的特殊特性。指令特性的值預期是單個 JavaScript 表示式 (v-for是例外情況,稍後我們再討論)。指令的職責是,當表示式的值改變時,將其產生的連帶影響,響應式地作用於 DOM。回顧我們在介紹中看到的例子:

<p v-if="seen">現在你看到我了</p>

這裡,v-if指令將根據表示式 seen 的值的真假來插入/移除<p>元素。

引數

一些指令能夠接收一個“引數”,在指令名稱之後以冒號表示。例如,v-bind指令可以用於響應式地更新 HTML 特性:

<a v-bind:href="url">...</a>

在這裡 href 是引數,告知 v-bind指令將該元素的 href特性與表示式 url的值繫結。

另一個例子是v-on 指令,它用於監聽 DOM 事件:

<a v-on:click="doSomething">...</a>

在這裡引數是監聽的事件名。我們也會更詳細地討論事件處理。

修飾符

修飾符 (Modifiers) 是以半形句號 . 指明的特殊字尾,用於指出一個指令應該以特殊方式繫結。例如,.prevent修飾符告訴 v-on指令對於觸發的事件呼叫event.preventDefault()

<form v-on:submit.prevent="onSubmit">...</form>

在接下來對v-onv-for等功能的探索中,你會看到修飾符的其它例子。

3.縮寫

v- 字首作為一種視覺提示,用來識別模板中 Vue 特定的特性。當你在使用 Vue.js 為現有標籤新增動態行為 (dynamic behavior) 時,v- 字首很有幫助,然而,對於一些頻繁用到的指令來說,就會感到使用繁瑣。同時,在構建由 Vue.js 管理所有模板的單頁面應用程式 (SPA - single page application)時,v-字首也變得沒那麼重要了。因此,Vue.js 為v-bindv-on這兩個最常用的指令,提供了特定簡寫:

v-bind縮寫

<!-- 完整語法 -->
<a v-bind:href="url">...</a>

<!-- 縮寫 -->
<a :href="url">...</a>

v-on 縮寫

<!-- 完整語法 -->
<a v-on:click="doSomething">...</a>

<!-- 縮寫 -->
<a @click="doSomething">...</a>

它們看起來可能與普通的 HTML 略有不同,但 : 與 @ 對於特性名來說都是合法字元,在所有支援 Vue.js 的瀏覽器都能被正確地解析。而且,它們不會出現在最終渲染的標記中。縮寫語法是完全可選的,但隨著你更深入地瞭解它們的作用,你會慶幸擁有它們。

計算屬性和偵聽器

計算屬性

模板內的表示式非常便利,但是設計它們的初衷是用於簡單運算的。在模板中放入太多的邏輯會讓模板過重且難以維護。例如:

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

在這個地方,模板不再是簡單的宣告式邏輯。你必須看一段時間才能意識到,這裡是想要顯示變數message的翻轉字串。當你想要在模板中多次引用此處的翻轉字串時,就會更加難以處理。

所以,對於任何複雜邏輯,你都應當使用計算屬性。

基礎例子

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 例項
      return this.message.split('').reverse().join('')
    }
  }
})

結果:

Original message: “Hello”

Computed reversed message: “olleH”

這裡我們聲明瞭一個計算屬性reversedMessage。我們提供的函式將用作屬性 vm.reversedMessage 的 getter 函式:

console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'

你可以開啟瀏覽器的控制檯,自行修改例子中的 vm。vm.reversedMessage的值始終取決於 vm.message 的值。

你可以像繫結普通屬性一樣在模板中繫結計算屬性。Vue 知道 vm.reversedMessage 依賴於 vm.message,因此當 vm.message發生改變時,所有依賴 vm.reversedMessage的繫結也會更新。而且最妙的是我們已經以宣告的方式建立了這種依賴關係:計算屬性的 getter 函式是沒有副作用 (side effect) 的,這使它更易於測試和理解。

計算屬性快取 vs 方法

你可能已經注意到我們可以通過在表示式中呼叫方法來達到同樣的效果:

<p>Reversed message: "{{ reversedMessage() }}"</p>
// 在元件中
methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

我們可以將同一函式定義為一個方法而不是一個計算屬性。兩種方式的最終結果確實是完全相同的。然而,不同的是計算屬性是基於它們的依賴進行快取的。只在相關依賴發生改變時它們才會重新求值。這就意味著只要 message還沒有發生改變,多次訪問reversedMessage 計算屬性會立即返回之前的計算結果,而不必再次執行函式。

這也同樣意味著下面的計算屬性將不再更新,因為 Date.now()不是響應式依賴:

computed: {
  now: function () {
    return Date.now()
  }
}

相比之下,每當觸發重新渲染時,呼叫方法將總會再次執行函式。

我們為什麼需要快取?假設我們有一個性能開銷比較大的計算屬性 A,它需要遍歷一個巨大的陣列並做大量的計算。然後我們可能有其他的計算屬性依賴於 A 。如果沒有快取,我們將不可避免的多次執行 A 的 getter!如果你不希望有快取,請用方法來替代。

計算屬性 vs 偵聽屬性

Vue 提供了一種更通用的方式來觀察和響應 Vue 例項上的資料變動:偵聽屬性。當你有一些資料需要隨著其它資料變動而變動時,你很容易濫用 watch——特別是如果你之前使用過 AngularJS。然而,通常更好的做法是使用計算屬性而不是命令式的 watch 回撥。細想一下這個例子:

<div id="demo">{{ fullName }}</div>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

上面程式碼是命令式且重複的。將它與計算屬性的版本進行比較:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

好得多了,不是嗎?

計算屬性的 setter

計算屬性預設只有 getter ,不過在需要時你也可以提供一個 setter :

// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

現在再執行 vm.fullName = 'John Doe' 時,setter 會被呼叫,vm.firstNamevm.lastName也會相應地被更新。

偵聽器

雖然計算屬性在大多數情況下更合適,但有時也需要一個自定義的偵聽器。這就是為什麼 Vue 通過watch選項提供了一個更通用的方法,來響應資料的變化。當需要在資料變化時執行非同步或開銷較大的操作時,這個方式是最有用的。

例如:

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>
<!-- 因為 AJAX 庫和通用工具的生態已經相當豐富,Vue 核心程式碼沒有重複 -->
<!-- 提供這些功能以保持精簡。這也可以讓你自由選擇自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 `question` 發生改變,這個函式就會執行
    question: function (newQuestion, oldQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    // `_.debounce` 是一個通過 Lodash 限制操作頻率的函式。
    // 在這個例子中,我們希望限制訪問 yesno.wtf/api 的頻率
    // AJAX 請求直到使用者輸入完畢才會發出。想要了解更多關於
    // `_.debounce` 函式 (及其近親 `_.throttle`) 的知識,
    // 請參考:https://lodash.com/docs#debounce
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = 'Questions usually contain a question mark. ;-)'
        return
      }
      this.answer = 'Thinking...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = 'Error! Could not reach the API. ' + error
        })
    }
  }
})
</script>

結果:

Ask a yes/no question:

I cannot give you an answer until you ask a question!
在這個示例中,使用watch選項允許我們執行非同步操作 (訪問一個 API),限制我們執行該操作的頻率,並在我們得到最終結果前,設定中間狀態。這些都是計算屬性無法做到的。