vue.js 2.x教程

教程

基礎

安裝

兼容性
Vue 不支持 IE8 及以下版本,因爲 Vue 使用了 IE8 無法模擬的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的瀏覽器。

更新日誌
最新穩定版本:2.5.13

每個版本的更新日誌見 GitHub

Vue Devtools

在使用 Vue 時,我們推薦在你的瀏覽器上安裝 Vue Devtools。它允許你在一個更友好的界面中審查和調試 Vue 應用。

直接用 <script> 引入

直接下載並用 <script> 標籤引入,Vue 會被註冊爲一個全局變量。

在開發環境下不要使用壓縮版本,不然你就失去了所有常見錯誤相關的警告!

CDN

我們推薦鏈接到一個你可以手動更新的指定版本號:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

你可以在 cdn.jsdelivr.net/npm/vue 瀏覽 NPM 包的源代碼。

Vue 也可以在 unpkgcdnjs 上獲取 (cdnjs 的版本更新可能略滯後)。

請確認瞭解不同構建版本並在你發佈的站點中使用生產環境版本,把 vue.js 換成 vue.min.js。這是一個更小的構建,可以帶來比開發環境下更快的速度體驗。

NPM

在用 Vue 構建大型應用時推薦使用 NPM 安裝[1]。NPM 能很好地和諸如 webpack 或 Browserify 模塊打包器配合使用。同時 Vue 也提供配套工具來開發單文件組件。

# 最新穩定版
$ npm install vue
命令行工具 (CLI)

Vue 提供一個官方命令行工具,可用於快速搭建大型單頁應用。該工具爲現代化的前端開發工作流提供了開箱即用的構建配置。只需幾分鐘即可創建並啓動一個帶熱重載、保存時靜態檢查以及可用於生產環境的構建配置的項目:

# 全局安裝 vue-cli
$ npm install --global vue-cli
# 創建一個基於 webpack 模板的新項目
$ vue init webpack my-project
# 安裝依賴,走你
$ cd my-project
$ npm install
$ npm run dev

CLI 工具假定用戶對 Node.js 和相關構建工具有一定程度的瞭解。如果你是新手,我們強烈建議先在不用構建工具的情況下通讀指南,在熟悉 Vue 本身之後再使用 CLI。

對不同構建版本的解釋

在 NPM 包的 dist/ 目錄你將會找到很多不同的 Vue.js 構建版本。這裏列出了它們之間的差別:

說明 UMD CommonJS ES Module
完整版 vue.js vue.common.js vue.esm.js
只包含運行時版 vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
完整版 (生產環境) vue.min.js - -
只包含運行時版 (生產環境) vue.runtime.min.js - -
術語
  • 完整版:同時包含編譯器和運行時的版本。
  • 編譯器:用來將模板字符串編譯成爲 JavaScript 渲染函數的代碼。
  • 運行時:用來創建 Vue 實例、渲染並處理虛擬 DOM 等的代碼。基本上就是除去編譯器的其它一切。
  • UMD:UMD 版本可以通過 <script> 標籤直接用在瀏覽器中。jsDelivr CDN 的 https://cdn.jsdelivr.net/npm/vue 默認文件就是運行時 + 編譯器的 UMD 版本 (vue.js)。
  • CommonJS:CommonJS 版本用來配合老的打包工具比如 Browserifywebpack 1。這些打包工具的默認文件 (pkg.main) 是隻包含運行時的 CommonJS 版本 (vue.runtime.common.js)。
  • ES Module:ES module 版本用來配合現代打包工具比如 webpack 2Rollup。這些打包工具的默認文件 (pkg.module) 是隻包含運行時的 ES Module 版本 (vue.runtime.esm.js)。
運行時 + 編譯器 vs. 只包含運行時

如果你需要在客戶端編譯模板 (比如傳入一個字符串給 template 選項,或掛載到一個元素上並以其 DOM 內部的 HTML 作爲模板),就將需要加上編譯器,即完整版:

// 需要編譯器
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 不需要編譯器
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

當使用 vue-loadervueify 的時候,*.vue 文件內部的模板會在構建時預編譯成 JavaScript。你在最終打好的包裏實際上是不需要編譯器的,所以只用運行時版本即可。

因爲運行時版本相比完整版體積要小大約 30%,所以應該儘可能使用這個版本。如果你仍然希望使用完整版,則需要在打包工具裏配置一個別名:

webpack

module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 時需用 'vue/dist/vue.common.js'
    }
  }
}

Rollup

const alias = require('rollup-plugin-alias')

rollup({
  // ...
  plugins: [
    alias({
      'vue': 'vue/dist/vue.esm.js'
    })
  ]
})

Browserify

添加到你項目的 package.json

{
  // ...
  "browser": {
    "vue": "vue/dist/vue.common.js"
  }
}
開發環境 vs. 生產環境模式

對於 UMD 版本來說,開發環境/生產環境模式是硬編碼好的:開發環境下用未壓縮的代碼,生產環境下使用壓縮後的代碼。

CommonJS 和 ES Module 版本是用於打包工具的,因此我們不提供壓縮後的版本。你需要自行將最終的包進行壓縮。

CommonJS 和 ES Module 版本同時保留原始的 process.env.NODE_ENV 檢測,以決定它們應該運行在什麼模式下。你應該使用適當的打包工具配置來替換這些環境變量以便控制 Vue 所運行的模式。把 process.env.NODE_ENV 替換爲字符串字面量同時可以讓 UglifyJS 之類的壓縮工具完全丟掉僅供開發環境的代碼塊,以減少最終的文件尺寸。

webpack

使用 webpack 的 DefinePlugin

var webpack = require('webpack')

module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    })
  ]
}

Rollup

使用 rollup-plugin-replace

const replace = require('rollup-plugin-replace')

rollup({
  // ...
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
}).then(...)

Browserify

爲你的包應用一次全局的 envify 轉換。

NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js

也可以移步生產環境部署。

CSP 環境

有些環境,如 Google Chrome Apps,會強制應用內容安全策略 (CSP),不能使用 new Function() 對錶達式求值。這時可以用 CSP 兼容版本。完整版本依賴於該功能來編譯模板,所以無法在這些環境下使用。

另一方面,運行時版本則是完全兼容 CSP 的。當通過 webpack + vue-loader 或者 Browserify + vueify 構建時,模板將被預編譯爲 render 函數,可以在 CSP 環境中完美運行。

開發版本

重要: GitHub 倉庫的 /dist 文件夾只有在新版本發佈時纔會提交。如果想要使用 GitHub 上 Vue 最新的源碼,你需要自己構建!

git clone https://github.com/vuejs/vue.git node_modules/vue
cd node_modules/vue
npm install
npm run build
Bower

Bower 只提供 UMD 版本。

# 最新穩定版本
$ bower install vue
AMD 模塊加載器

所有 UMD 版本都可以直接用作 AMD 模塊。

譯者注
對於中國大陸用戶,建議將 NPM 源設置爲國內的鏡像,可以大幅提升安裝速度。

介紹

Vue.js 是什麼

Vue (讀音 /vjuː/,類似於 view) 是一套用於構建用戶界面的漸進式框架。與其它大型框架不同的是,Vue 被設計爲可以自底向上逐層應用。Vue 的核心庫只關注視圖層,不僅易於上手,還便於與第三方庫或既有項目整合。另一方面,當與現代化的工具鏈以及各種支持類庫結合使用時,Vue 也完全能夠爲複雜的單頁應用提供驅動。

如果你想在深入學習 Vue 之前對它有更多瞭解,我們製作了一個視頻,帶您瞭解其核心概念和一個示例工程。

如果你已經是有經驗的前端開發者,想知道 Vue 與其它庫/框架有哪些區別,請查看對比其它框架。

起步

官方指南假設你已瞭解關於 HTML、CSS 和 JavaScript 的中級知識。如果你剛開始學習前端開發,將框架作爲你的第一步可能不是最好的主意——掌握好基礎知識再來吧!之前有其它框架的使用經驗會有幫助,但這不是必需的。

嘗試 Vue.js 最簡單的方法是使用 JSFiddle 上的 Hello World 例子。你可以在瀏覽器新標籤頁中打開它,跟着例子學習一些基礎用法。或者你也可以創建一個 .html 文件,然後通過如下方式引入 Vue:

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

安裝教程給出了更多安裝 Vue 的方式。請注意我們不推薦新手直接使用 vue-cli,尤其是在你還不熟悉基於 Node.js 的構建工具時。

聲明式渲染

Vue.js 的核心是一個允許採用簡潔的模板語法來聲明式地將數據渲染進 DOM 的系統:

<div id="app">
  {{ message }}
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

我們已經成功創建了第一個 Vue 應用!看起來這跟渲染一個字符串模板非常類似,但是 Vue 在背後做了大量工作。現在數據和 DOM 已經被建立了關聯,所有東西都是響應式的。我們要怎麼確認呢?打開你的瀏覽器的 JavaScript 控制檯 (就在這個頁面打開),並修改 app.message 的值,你將看到上例相應地更新。

除了文本插值,我們還可以像這樣來綁定元素特性:

<div id="app-2">
  <span v-bind:title="message">
    鼠標懸停幾秒鐘查看此處動態綁定的提示信息!
  </span>
</div>
var app2 = new Vue({
  el: '#app-2',
  data: {
    message: '頁面加載於 ' + new Date().toLocaleString()
  }
})

這裏我們遇到了一點新東西。你看到的 v-bind 特性被稱爲指令。指令帶有前綴 v-,以表示它們是 Vue 提供的特殊特性。可能你已經猜到了,它們會在渲染的 DOM 上應用特殊的響應式行爲。在這裏,該指令的意思是:“將這個元素節點的 title 特性和 Vue 實例的 message 屬性保持一致”。

如果你再次打開瀏覽器的 JavaScript 控制檯,輸入 app2.message = '新消息',就會再一次看到這個綁定了 title 特性的 HTML 已經進行了更新。

條件與循環

控制切換一個元素是否顯示也相當簡單:

<div id="app-3">
  <p v-if="seen">現在你看到我了</p>
</div>
var app3 = new Vue({
  el: '#app-3',
  data: {
    seen: true
  }
})

繼續在控制檯輸入 “`app3.seen = false,你會發現之前顯示的消息消失了。

這個例子演示了我們不僅可以把數據綁定到 DOM 文本或特性,還可以綁定到 DOM 結構。此外,Vue 也提供一個強大的過渡效果系統,可以在 Vue 插入/更新/移除元素時自動應用過渡效果。

還有其它很多指令,每個都有特殊的功能。例如,v-for 指令可以綁定數組的數據來渲染一個項目列表:

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>
var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: '學習 JavaScript' },
      { text: '學習 Vue' },
      { text: '整個牛項目' }
    ]
  }
})

在控制檯裏,輸入 app4.todos.push({ text: '新項目' }),你會發現列表最後添加了一個新項目。

處理用戶輸入

爲了讓用戶和你的應用進行交互,我們可以用 v-on 指令添加一個事件監聽器,通過它調用在 Vue 實例中定義的方法:

<div id="app-5">
  <p>{{ message }}</p>
  <button v-on:click="reverseMessage">逆轉消息</button>
</div>
var app5 = new Vue({
  el: '#app-5',
  data: {
    message: 'Hello Vue.js!'
  },
  methods: {
    reverseMessage: function () {
      this.message = this.message.split('').reverse().join('')
    }
  }
})

注意在 reverseMessage 方法中,我們更新了應用的狀態,但沒有觸碰 DOM——所有的 DOM 操作都由 Vue 來處理,你編寫的代碼只需要關注邏輯層面即可。

Vue 還提供了 v-model 指令,它能輕鬆實現表單輸入和應用狀態之間的雙向綁定。

<div id="app-6">
  <p>{{ message }}</p>
  <input v-model="message">
</div>
var app6 = new Vue({
  el: '#app-6',
  data: {
    message: 'Hello Vue!'
  }
})
組件化應用構建

組件系統是 Vue 的另一個重要概念,因爲它是一種抽象,允許我們使用小型、獨立和通常可複用的組件構建大型應用。仔細想想,幾乎任意類型的應用界面都可以抽象爲一個組件樹:

在 Vue 裏,一個組件本質上是一個擁有預定義選項的一個 Vue 實例。在 Vue 中註冊組件很簡單:

// 定義名爲 todo-item 的新組件
Vue.component('todo-item', {
  template: '<li>這是個待辦項</li>'
})

現在你可以用它構建另一個組件模板:

<ol>
  <!-- 創建一個 todo-item 組件的實例 -->
  <todo-item></todo-item>
</ol>

但是這樣會爲每個待辦項渲染同樣的文本,這看起來並不炫酷。我們應該能從父作用域將數據傳到子組件纔對。讓我們來修改一下組件的定義,使之能夠接受一個 prop:

Vue.component('todo-item', {
  // todo-item 組件現在接受一個
  // "prop",類似於一個自定義特性。
  // 這個 prop 名爲 todo。
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})

現在,我們可以使用 v-bind 指令將待辦項傳到循環輸出的每個組件中:

<div id="app-7">
  <ol>
    <!--
      現在我們爲每個 todo-item 提供 todo 對象
      todo 對象是變量,即其內容可以是動態的。
      我們也需要爲每個組件提供一個“key”,稍後再
      作詳細解釋。
    -->
    <todo-item
      v-for="item in groceryList"
      v-bind:todo="item"
      v-bind:key="item.id">
    </todo-item>
  </ol>
</div>
Vue.component('todo-item', {
  props: ['todo'],
  template: '<li>{{ todo.text }}</li>'
})

var app7 = new Vue({
  el: '#app-7',
  data: {
    groceryList: [
      { id: 0, text: '蔬菜' },
      { id: 1, text: '奶酪' },
      { id: 2, text: '隨便其它什麼人喫的東西' }
    ]
  }
})

儘管這只是一個刻意設計的例子,但是我們已經設法將應用分割成了兩個更小的單元。子單元通過 prop 接口與父單元進行了良好的解耦。我們現在可以進一步改進 <todo-item> 組件,提供更爲複雜的模板和邏輯,而不會影響到父單元。

在一個大型應用中,有必要將整個應用程序劃分爲組件,以使開發更易管理。在後續教程中我們將詳述組件,不過這裏有一個 (假想的) 例子,以展示使用了組件的應用模板是什麼樣的:

<div id="app">
  <app-nav></app-nav>
  <app-view>
    <app-sidebar></app-sidebar>
    <app-content></app-content>
  </app-view>
</div>
與自定義元素的關係

你可能已經注意到 Vue 組件非常類似於自定義元素——它是 Web 組件規範的一部分,這是因爲 Vue 的組件語法部分參考了該規範。例如 Vue 組件實現了 Slot APIis 特性。但是,還是有幾個關鍵差別:

  1. Web 組件規範仍然處於草案階段,並且未被所有瀏覽器原生實現。相比之下,Vue 組件不需要任何 polyfill,並且在所有支持的瀏覽器 (IE9 及更高版本) 之下表現一致。必要時,Vue 組件也可以包裝於原生自定義元素之內。
  2. Vue 組件提供了純自定義元素所不具備的一些重要功能,最突出的是跨組件數據流、自定義事件通信以及構建工具集成。
準備好了嗎?

我們剛纔簡單介紹了 Vue 核心最基本的功能——本教程的其餘部分將涵蓋這些功能以及其它高級功能更詳細的細節,所以請務必讀完整個教程!

Vue 實例

創建一個 Vue 實例

每個 Vue 應用都是通過用 Vue 函數創建一個新的 Vue 實例開始的:

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

雖然沒有完全遵循 MVVM 模型,但是 Vue 的設計也受到了它的啓發。因此在文檔中經常會使用 vm (ViewModel 的縮寫) 這個變量名錶示 Vue 實例。

當創建一個 Vue 實例時,你可以傳入一個選項對象。這篇教程主要描述的就是如何使用這些選項來創建你想要的行爲。作爲參考,你也可以在 API 文檔 中瀏覽完整的選項列表。

一個 Vue 應用由一個通過 new Vue 創建的根 Vue 實例,以及可選的嵌套的、可複用的組件樹組成。舉個例子,一個 todo 應用的組件樹可以是這樣的:

根實例
└─ TodoList
   ├─ TodoItem
   │  ├─ DeleteTodoButton
   │  └─ EditTodoButton
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

我們會在稍後的組件系統章節具體展開。不過現在,你只需要明白所有的 Vue 組件都是 Vue 實例,並且接受相同的選項對象 (一些根實例特有的選項除外)。

數據與方法

當一個 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 () {
    return {
      obj
    }
  }
})
<div id="app">
  <p>{{ obj.foo }}</p>
  <!-- 這裏的 `obj.foo` 不會更新! -->
  <button @click="obj.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` 改變後調用
})

以後你可以在 API 參考中查閱到完整的實例屬性和方法的列表。

實例生命週期鉤子

每個 Vue 實例在被創建時都要經過一系列的初始化過程——例如,需要設置數據監聽、編譯模板、將實例掛載到 DOM 並在數據變化時更新 DOM 等。同時在這個過程中也會運行一些叫做生命週期鉤子的函數,這給了用戶在不同階段添加自己的代碼的機會。

比如 create 鉤子可以用來在一個實例被創建之後執行代碼:

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

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

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

生命週期圖示

下圖展示了實例的生命週期。你不需要立馬弄明白所有的東西,不過隨着你的不斷學習和使用,它的參考價值會越來越高。

模板語法

Vue.js 使用了基於 HTML 的模板語法,允許開發者聲明式地將 DOM 綁定至底層 Vue 實例的數據。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循規範的瀏覽器和 HTML 解析器解析。

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

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

插值
文本

數據綁定最常見的形式就是使用“Mustache”語法 (雙大括號) 的文本插值:

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

Mustache 標籤將會被替代爲對應數據對象上 msg 屬性的值。無論何時,綁定的數據對象上 msg 屬性發生了改變,插值處的內容都會更新。

通過使用 v-once 指令,你也能執行一次性地插值,當數據改變時,插值處的內容不會更新。但請留心這會影響到該節點上所有的數據綁定:

<span v-once>這個將不會改變: {{ msg }}</span>
原始 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 。你不應該在模板表達式中試圖訪問用戶定義的全局變量。

指令

指令 (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 等功能的探索中,你會看到修飾符的其它例子。

縮寫

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.getAnswer()
    }
  },
  methods: {
    // `_.debounce` 是一個通過 Lodash 限制操作頻率的函數。
    // 在這個例子中,我們希望限制訪問 yesno.wtf/api 的頻率
    // AJAX 請求直到用戶輸入完畢纔會發出。想要了解更多關於
    // `_.debounce` 函數 (及其近親 `_.throttle`) 的知識,
    // 請參考:https://lodash.com/docs#debounce
    getAnswer: _.debounce(
      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
          })
      },
      // 這是我們爲判定用戶停止輸入等待的毫秒數
      500
    )
  }
})
</script>

在這個示例中,使用 watch 選項允許我們執行異步操作 (訪問一個 API),限制我們執行該操作的頻率,並在我們得到最終結果前,設置中間狀態。這些都是計算屬性無法做到的。

除了 watch 選項之外,您還可以使用命令式的 vm.$watch API

Class 與 Style 綁定

操作元素的 class 列表和內聯樣式是數據綁定的一個常見需求。因爲它們都是屬性,所以我們可以用 v-bind 處理它們:只需要通過表達式計算出字符串結果即可。不過,字符串拼接麻煩且易錯。因此,在將 v-bind 用於 classstyle 時,Vue.js 做了專門的增強。表達式結果的類型除了字符串之外,還可以是對象或數組。

綁定 HTML Class
對象語法

我們可以傳給 v-bind:class 一個對象,以動態地切換 class:

<div v-bind:class="{ active: isActive }"></div>

上面的語法表示 active 這個 class 存在與否將取決於數據屬性 isActivetruthiness

你可以在對象中傳入更多屬性來動態切換多個 class。此外,v-bind:class 指令也可以與普通的 class 屬性共存。當有如下模板:

<div class="static"
     v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>

和如下 data:

data: {
  isActive: true,
  hasError: false
}

結果渲染爲:

<div class="static active"></div>

isActive 或者 hasError 變化時,class 列表將相應地更新。例如,如果 hasError 的值爲 true,class 列表將變爲 "static active text-danger"

綁定的數據對象不必內聯定義在模板裏:

<div v-bind:class="classObject"></div>
data: {
  classObject: {
    active: true,
    'text-danger': false
  }
}

渲染的結果和上面一樣。我們也可以在這裏綁定一個返回對象的計算屬性。這是一個常用且強大的模式:

<div v-bind:class="classObject"></div>
data: {
  isActive: true,
  error: null
},
computed: {
  classObject: function () {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}
數組語法

我們可以把一個數組傳給 v-bind:class,以應用一個 class 列表:

<div v-bind:class="[activeClass, errorClass]"></div>
data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

渲染爲:

<div class="active text-danger"></div>

如果你也想根據條件切換列表中的 class,可以用三元表達式:

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>

這樣寫將始終添加 errorClass,但是隻有在 isActive 是 truthy[1] 時才添加 activeClass

不過,當有多個條件 class 時這樣寫有些繁瑣。所以在數組語法中也可以使用對象語法:

<div v-bind:class="[{ active: isActive }, errorClass]"></div>
用在組件上

這個章節假設你已經對 Vue 組件有一定的瞭解。當然你也可以先跳過這裏,稍後再回過頭來看。

當在一個自定義組件上使用 class 屬性時,這些類將被添加到該組件的根元素上面。這個元素上已經存在的類不會被覆蓋。

例如,如果你聲明瞭這個組件:

Vue.component('my-component', {
  template: '<p class="foo bar">Hi</p>'
})

然後在使用它的時候添加一些 class:

<my-component class="baz boo"></my-component>

HTML 將被渲染爲:

<p class="foo bar baz boo">Hi</p>

對於帶數據綁定 class 也同樣適用:

<my-component v-bind:class="{ active: isActive }"></my-component>

isActive 爲 truthy[1] 時,HTML 將被渲染成爲:

<p class="foo bar active">Hi</p>
綁定內聯樣式
對象語法

v-bind:style 的對象語法十分直觀——看着非常像 CSS,但其實是一個 JavaScript 對象。CSS 屬性名可以用駝峯式 (camelCase) 或短橫線分隔 (kebab-case,記得用單引號括起來) 來命名:

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
  activeColor: 'red',
  fontSize: 30
}

直接綁定到一個樣式對象通常更好,這會讓模板更清晰:

<div v-bind:style="styleObject"></div>
data: {
  styleObject: {
    color: 'red',
    fontSize: '13px'
  }
}

同樣的,對象語法常常結合返回對象的計算屬性使用。

數組語法

v-bind:style 的數組語法可以將多個樣式對象應用到同一個元素上:

<div v-bind:style="[baseStyles, overridingStyles]"></div>
自動添加前綴

v-bind:style 使用需要添加瀏覽器引擎前綴的 CSS 屬性時,如 transform,Vue.js 會自動偵測並添加相應的前綴。

多重值

2.3.0+

從 2.3.0 起你可以爲 style 綁定中的屬性提供一個包含多個值的數組,常用於提供多個帶前綴的值,例如:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

這樣寫只會渲染數組中最後一個被瀏覽器支持的值。在本例中,如果瀏覽器支持不帶瀏覽器前綴的 flexbox,那麼就只會渲染 display: flex

譯者注

[1] truthy 不是 true,詳見 MDN 的解釋。

條件渲染

v-if

在字符串模板中,比如 Handlebars,我們得像這樣寫一個條件塊:

<!-- Handlebars 模板 -->
{{#if ok}}
  <h1>Yes</h1>
{{/if}}

在 Vue 中,我們使用 v-if 指令實現同樣的功能:

<h1 v-if="ok">Yes</h1>

也可以用 v-else 添加一個“else 塊”:

<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>
<template> 元素上使用 v-if 條件渲染分組

因爲 v-if 是一個指令,所以必須將它添加到一個元素上。但是如果想切換多個元素呢?此時可以把一個 <template> 元素當做不可見的包裹元素,並在上面使用 v-if。最終的渲染結果將不包含 <template> 元素。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>
v-else

你可以使用 v-else 指令來表示 v-if 的“else 塊”:

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>

v-else 元素必須緊跟在帶 v-if 或者 v-else-if 的元素的後面,否則它將不會被識別。

v-else-if

2.1.0 新增

v-else-if,顧名思義,充當 v-if 的“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-elsev-else-if 也必須緊跟在帶 v-if 或者 v-else-if 的元素之後。

key 管理可複用的元素

Vue 會盡可能高效地渲染元素,通常會複用已有元素而不是從頭開始渲染。這麼做除了使 Vue 變得非常快之外,還有其它一些好處。例如,如果你允許用戶在不同的登錄方式之間切換:

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address">
</template>

那麼在上面的代碼中切換 loginType 將不會清除用戶已經輸入的內容。因爲兩個模板使用了相同的元素,<input> 不會被替換掉——僅僅是替換了它的 placeholder

這樣也不總是符合實際需求,所以 Vue 爲你提供了一種方式來表達“這兩個元素是完全獨立的,不要複用它們”。只需添加一個具有唯一值的 key 屬性即可:

現在,每次切換時,輸入框都將被重新渲染。

注意,<label> 元素仍然會被高效地複用,因爲它們沒有添加 key 屬性。

v-show

另一個用於根據條件展示元素的選項是 v-show 指令。用法大致一樣:

<h1 v-show="ok">Hello!</h1>

不同的是帶有 v-show 的元素始終會被渲染並保留在 DOM 中。v-show 只是簡單地切換元素的 CSS 屬性 display

注意,v-show 不支持 <template> 元素,也不支持 v-else

v-if vs v-show

v-if 是“真正”的條件渲染,因爲它會確保在切換過程中條件塊內的事件監聽器和子組件適當地被銷燬和重建。

v-if 也是惰性的:如果在初始渲染時條件爲假,則什麼也不做——直到條件第一次變爲真時,纔會開始渲染條件塊。

相比之下,v-show 就簡單得多——不管初始條件是什麼,元素總是會被渲染,並且只是簡單地基於 CSS 進行切換。

一般來說,v-if 有更高的切換開銷,而 v-show 有更高的初始渲染開銷。因此,如果需要非常頻繁地切換,則使用 v-show 較好;如果在運行時條件很少改變,則使用 v-if 較好。

v-ifv-for 一起使用

v-ifv-for 一起使用時,v-for 具有比 v-if 更高的優先級。

列表渲染

v-for 把一個數組對應爲一組元素

我們用 v-for 指令根據一組數組的選項列表進行渲染。v-for 指令需要使用 item in items 形式的特殊語法,items 是源數據數組並且 item 是數組元素迭代的別名。

<ul id="example-1">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>
var example1 = new Vue({
  el: '#example-1',
  data: {
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

v-for 塊中,我們擁有對父作用域屬性的完全訪問權限。v-for 還支持一個可選的第二個參數爲當前項的索引。

<ul id="example-2">
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>
var example2 = new Vue({
  el: '#example-2',
  data: {
    parentMessage: 'Parent',
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

你也可以用 of 替代 in 作爲分隔符,因爲它是最接近 JavaScript 迭代器的語法:

<div v-for="item of items"></div>
一個對象的 v-for

你也可以用 v-for 通過一個對象的屬性來迭代。

<ul id="v-for-object" class="demo">
  <li v-for="value in object">
    {{ value }}
  </li>
</ul>
new Vue({
  el: '#v-for-object',
  data: {
    object: {
      firstName: 'John',
      lastName: 'Doe',
      age: 30
    }
  }
})

你也可以提供第二個的參數爲鍵名:

<div v-for="(value, key) in object">
  {{ key }}: {{ value }}
</div>

第三個參數爲索引:

<div v-for="(value, key, index) in object">
  {{ index }}. {{ key }}: {{ value }}
</div>

在遍歷對象時,是按 Object.keys() 的結果遍歷,但是不能保證它的結果在不同的 JavaScript 引擎下是一致的。

key

當 Vue.js 用 v-for 正在更新已渲染過的元素列表時,它默認用“就地複用”策略。如果數據項的順序被改變,Vue 將不會移動 DOM 元素來匹配數據項的順序, 而是簡單複用此處每個元素,並且確保它在特定索引下顯示已被渲染過的每個元素。這個類似 Vue 1.x 的 track-by="$index"

這個默認的模式是高效的,但是隻適用於不依賴子組件狀態或臨時 DOM 狀態 (例如:表單輸入值) 的列表渲染輸出

爲了給 Vue 一個提示,以便它能跟蹤每個節點的身份,從而重用和重新排序現有元素,你需要爲每項提供一個唯一 key 屬性。理想的 key 值是每項都有的且唯一的 id。這個特殊的屬性相當於 Vue 1.x 的 track-by ,但它的工作方式類似於一個屬性,所以你需要用 v-bind 來綁定動態值 (在這裏使用簡寫):

<div v-for="item in items" :key="item.id">
  <!-- 內容 -->
</div>

建議儘可能在使用 v-for 時提供 key,除非遍歷輸出的 DOM 內容非常簡單,或者是刻意依賴默認行爲以獲取性能上的提升。

因爲它是 Vue 識別節點的一個通用機制,key 並不與 v-for 特別關聯,key 還具有其他用途,我們將在後面的指南中看到其他用途。

數組更新檢測
變異方法

Vue 包含一組觀察數組的變異方法,所以它們也將會觸發視圖更新。這些方法如下:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

你打開控制檯,然後用前面例子的 items 數組調用變異方法:example1.items.push({ message: 'Baz' })

替換數組

變異方法 (mutation method),顧名思義,會改變被這些方法調用的原始數組。相比之下,也有非變異 (non-mutating method) 方法,例如:filter(), concat()slice() 。這些不會改變原始數組,但總是返回一個新數組。當使用非變異方法時,可以用新數組替換舊數組:

example1.items = example1.items.filter(function (item) {
  return item.message.match(/Foo/)
})

你可能認爲這將導致 Vue 丟棄現有 DOM 並重新渲染整個列表。幸運的是,事實並非如此。Vue 爲了使得 DOM 元素得到最大範圍的重用而實現了一些智能的、啓發式的方法,所以用一個含有相同元素的數組去替換原來的數組是非常高效的操作。

注意事項

由於 JavaScript 的限制,Vue 不能檢測以下變動的數組:

  1. 當你利用索引直接設置一個項時,例如:vm.items[indexOfItem] = newValue
  2. 當你修改數組的長度時,例如:vm.items.length = newLength

爲了解決第一類問題,以下兩種方式都可以實現和 vm.items[indexOfItem] = newValue 相同的效果,同時也將觸發狀態更新:

// Vue.set
Vue.set(example1.items, indexOfItem, newValue)
// Array.prototype.splice
example1.items.splice(indexOfItem, 1, newValue)

爲了解決第二類問題,你可以使用 splice

example1.items.splice(newLength)
對象更改檢測注意事項

還是由於 JavaScript 的限制,Vue 不能檢測對象屬性的添加或刪除:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` 現在是響應式的

vm.b = 2
// `vm.b` 不是響應式的

對於已經創建的實例,Vue 不能動態添加根級別的響應式屬性。但是,可以使用 Vue.set(object, key, value) 方法向嵌套對象添加響應式屬性。例如,對於:

var vm = new Vue({
  data: {
    userProfile: {
      name: 'Anika'
    }
  }
})

你可以添加一個新的 age 屬性到嵌套的 userProfile 對象:

Vue.set(vm.userProfile, 'age', 27)

你還可以使用 vm.$set 實例方法,它只是全局 Vue.set 的別名:

vm.$set(vm.userProfile, 'age', 27)

有時你可能需要爲已有對象賦予多個新屬性,比如使用 Object.assign()_.extend()。在這種情況下,你應該用兩個對象的屬性創建一個新的對象。所以,如果你想添加新的響應式屬性,不要像這樣:

Object.assign(vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

你應該這樣做:

vm.userProfile = Object.assign({}, vm.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})
顯示過濾/排序結果

有時,我們想要顯示一個數組的過濾或排序副本,而不實際改變或重置原始數據。在這種情況下,可以創建返回過濾或排序數組的計算屬性。

例如:

<li v-for="n in evenNumbers">{{ n }}</li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}

在計算屬性不適用的情況下 (例如,在嵌套 v-for 循環中) 你可以使用一個 method 方法:

<li v-for="n in even(numbers)">{{ n }}</li>
data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
methods: {
  even: function (numbers) {
    return numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}
一段取值範圍的 v-for

v-for 也可以取整數。在這種情況下,它將重複多次模板。

<div>
  <span v-for="n in 10">{{ n }} </span>
</div>
v-for on a <template>

類似於 v-if,你也可以利用帶有 v-for<template> 渲染多個元素。比如:

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider"></li>
  </template>
</ul>
v-for with v-if

當它們處於同一節點,v-for 的優先級比 v-if 更高,這意味着 v-if 將分別重複運行於每個 v-for 循環中。當你想爲僅有的一些項渲染節點時,這種優先級的機制會十分有用,如下:

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>

上面的代碼只傳遞了未完成的 todos。

而如果你的目的是有條件地跳過循環的執行,那麼可以將 v-if 置於外層元素 (或 <template>)上。如:

<ul v-if="todos.length">
  <li v-for="todo in todos">
    {{ todo }}
  </li>
</ul>
<p v-else>No todos left!</p>
一個組件的 v-for

瞭解組件相關知識,查看 組件。完全可以先跳過它,以後再回來查看。

在自定義組件裏,你可以像任何普通元素一樣用 v-for

<my-component v-for="item in items" :key="item.id"></my-component>

2.2.0+ 的版本里,當在組件中使用 v-for 時,key 現在是必須的。

然而,任何數據都不會被自動傳遞到組件裏,因爲組件有自己獨立的作用域。爲了把迭代數據傳遞到組件裏,我們要用 props

<my-component
  v-for="(item, index) in items"
  v-bind:item="item"
  v-bind:index="index"
  v-bind:key="item.id"
></my-component>

不自動將 item 注入到組件裏的原因是,這會使得組件與 v-for 的運作緊密耦合。明確組件數據的來源能夠使組件在其他場合重複使用。

下面是一個簡單的 todo list 的完整例子:

<div id="todo-list-example">
  <input
    v-model="newTodoText"
    v-on:keyup.enter="addNewTodo"
    placeholder="Add a todo"
  >
  <ul>
    <li
      is="todo-item"
      v-for="(todo, index) in todos"
      v-bind:key="todo.id"
      v-bind:title="todo.title"
      v-on:remove="todos.splice(index, 1)"
    ></li>
  </ul>
</div>

注意這裏的 is="todo-item" 屬性。這種做法在使用 DOM 模板時是十分必要的,因爲在 <ul> 元素內只有 <li> 元素會被看作有效內容。這樣做實現的效果與 <todo-item> 相同,但是可以避開一些潛在的瀏覽器解析錯誤。查看 DOM 模板解析說明 來了解更多信息。

Vue.component('todo-item', {
  template: '\
    <li>\
      {{ title }}\
      <button v-on:click="$emit(\'remove\')">X</button>\
    </li>\
  ',
  props: ['title']
})

new Vue({
  el: '#todo-list-example',
  data: {
    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: function () {
      this.todos.push({
        id: this.nextTodoId++,
        title: this.newTodoText
      })
      this.newTodoText = ''
    }
  }
})

事件處理

監聽事件

可以用 v-on 指令監聽 DOM 事件,並在觸發時運行一些 JavaScript 代碼。

示例:

<div id="example-1">
  <button v-on:click="counter += 1">Add 1</button>
  <p>The button above has been clicked {{ counter }} times.</p>
</div>
var example1 = new Vue({
  el: '#example-1',
  data: {
    counter: 0
  }
})
事件處理方法

然而許多事件處理邏輯會更爲複雜,所以直接把 JavaScript 代碼寫在 v-on 指令中是不可行的。因此 v-on 還可以接收一個需要調用的方法名稱。

示例:

<div id="example-2">
  <!-- `greet` 是在下面定義的方法名 -->
  <button v-on:click="greet">Greet</button>
</div>
var example2 = new Vue({
  el: '#example-2',
  data: {
    name: 'Vue.js'
  },
  // 在 `methods` 對象中定義方法
  methods: {
    greet: function (event) {
      // `this` 在方法裏指向當前 Vue 實例
      alert('Hello ' + this.name + '!')
      // `event` 是原生 DOM 事件
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
})

// 也可以用 JavaScript 直接調用方法
example2.greet() // => 'Hello Vue.js!'
內聯處理器中的方法

除了直接綁定到一個方法,也可以在內聯 JavaScript 語句中調用方法:

<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>

有時也需要在內聯語句處理器中訪問原始的 DOM 事件。可以用特殊變量 $event 把它傳入方法:

<button v-on:click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>
// ...
methods: {
  warn: function (message, event) {
    // 現在我們可以訪問原生事件對象
    if (event) event.preventDefault()
    alert(message)
  }
}
事件修飾符

在事件處理程序中調用 event.preventDefault()event.stopPropagation() 是非常常見的需求。儘管我們可以在方法中輕鬆實現這點,但更好的方式是:方法只有純粹的數據邏輯,而不是去處理 DOM 事件細節。

爲了解決這個問題,Vue.js 爲 v-on 提供了事件修飾符。之前提過,修飾符是由點開頭的指令後綴來表示的。

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
<!-- 阻止單擊事件繼續傳播 -->
<a v-on:click.stop="doThis"></a>

<!-- 提交事件不再重載頁面 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 修飾符可以串聯 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 只有修飾符 -->
<form v-on:submit.prevent></form>

<!-- 添加事件監聽器時使用事件捕獲模式 -->
<!-- 即元素自身觸發的事件先在此處處理,然後才交由內部元素進行處理 -->
<div v-on:click.capture="doThis">...</div>

<!-- 只當在 event.target 是當前元素自身時觸發處理函數 -->
<!-- 即事件不是從內部元素觸發的 -->
<div v-on:click.self="doThat">...</div>

使用修飾符時,順序很重要;相應的代碼會以同樣的順序產生。因此,用 @click.prevent.self 會阻止所有的點擊,而 @click.self.prevent 只會阻止對元素自身的點擊。

2.1.4 新增

<!-- 點擊事件將只會觸發一次 -->
<a v-on:click.once="doThis"></a>

不像其它只能對原生的 DOM 事件起作用的修飾符,.once 修飾符還能被用到自定義的組件事件上。如果你還沒有閱讀關於組件的文檔,現在大可不必擔心。

2.3.0 新增

Vue 還對應 addEventListener 中的 passive 選項提供了 .passive 修飾符。

<!-- 滾動事件的默認行爲 (即滾動行爲) 將會立即觸發 -->
<!-- 而不會等待 `onScroll` 完成  -->
<!-- 這其中包含 `event.preventDefault()` 的情況 -->
<div v-on:scroll.passive="onScroll">...</div>

這個 .passive 修飾符尤其能夠提升移動端的性能。

不要把 .passive.prevent 一起使用,因爲 .prevent 將會被忽略,同時瀏覽器可能會向你展示一個警告。請記住,.passive 會告訴瀏覽器你不想阻止事件的默認行爲。

按鍵修飾符

在監聽鍵盤事件時,我們經常需要檢查常見的鍵值。Vue 允許爲 v-on 在監聽鍵盤事件時添加按鍵修飾符:

<!-- 只有在 `keyCode` 是 13 時調用 `vm.submit()` -->
<input v-on:keyup.13="submit">

記住所有的 keyCode 比較困難,所以 Vue 爲最常用的按鍵提供了別名:

<!-- 同上 -->
<input v-on:keyup.enter="submit">

<!-- 縮寫語法 -->
<input @keyup.enter="submit">

全部的按鍵別名:

  • .enter
  • .tab
  • .delete (捕獲“刪除”和“退格”鍵)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

可以通過全局 config.keyCodes 對象自定義按鍵修飾符別名

// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
自動匹配按鍵修飾符

2.5.0 新增

你也可直接將 KeyboardEvent.key 暴露的任意有效按鍵名轉換爲 kebab-case 來作爲修飾符:

<input @keyup.page-down="onPageDown">

在上面的例子中,處理函數僅在 $event.key === 'PageDown' 時被調用。

有一些按鍵 (.esc 以及所有的方向鍵) 在 IE9 中有不同的 key 值, 如果你想支持 IE9,它們的內置別名應該是首選。

系統修飾鍵

2.1.0 新增

可以用如下修飾符來實現僅在按下相應按鍵時才觸發鼠標或鍵盤事件的監聽器。

  • .ctrl
  • .alt
  • .shift
  • .meta

注意:在 Mac 系統鍵盤上,meta 對應 command 鍵 (⌘)。在 Windows 系統鍵盤 meta 對應 Windows 徽標鍵 (⊞)。在 Sun 操作系統鍵盤上,meta 對應實心寶石鍵 (◆)。在其他特定鍵盤上,尤其在 MIT 和 Lisp 機器的鍵盤、以及其後繼產品,比如 Knight 鍵盤、space-cadet 鍵盤,meta 被標記爲“META”。在 Symbolics 鍵盤上,meta 被標記爲“META”或者“Meta”。

例如:

<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

請注意修飾鍵與常規按鍵不同,在和 keyup 事件一起用時,事件觸發時修飾鍵必須處於按下狀態。換句話說,只有在按住 ctrl 的情況下釋放其它按鍵,才能觸發 keyup.ctrl。而單單釋放 ctrl 也不會觸發事件。

.exact 修飾符

2.5.0 新增

.exact 修飾符允許你控制由精確的系統修飾符組合觸發的事件。

<!-- 即使 Alt 或 Shift 被一同按下時也不會觸發 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的時候才觸發 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 沒有任何系統修飾符被按下的時候才觸發 -->
<button @click.exact="onClick">A</button>
鼠標按鈕修飾符

2.2.0 新增

  • .left
  • .right
  • .middle

這些修飾符會限制處理函數僅響應特定的鼠標按鈕。

爲什麼在 HTML 中監聽事件?

你可能注意到這種事件監聽的方式違背了關注點分離 (separation of concern) 這個長期以來的優良傳統。但不必擔心,因爲所有的 Vue.js 事件處理方法和表達式都嚴格綁定在當前視圖的 ViewModel 上,它不會導致任何維護上的困難。實際上,使用 v-on 有幾個好處:

  1. 掃一眼 HTML 模板便能輕鬆定位在 JavaScript 代碼裏對應的方法。
  2. 因爲你無須在 JavaScript 裏手動綁定事件,你的 ViewModel 代碼可以是非常純粹的邏輯,和 DOM 完全解耦,更易於測試。
  3. 當一個 ViewModel 被銷燬時,所有的事件處理器都會自動被刪除。你無須擔心如何自己清理它們。

表單輸入綁定

基礎用法

你可以用 v-model 指令在表單 <input><textarea> 元素上創建雙向數據綁定。它會根據控件類型自動選取正確的方法來更新元素。儘管有些神奇,但 v-model 本質上不過是語法糖。它負責監聽用戶的輸入事件以更新數據,並對一些極端場景進行一些特殊處理。

v-model 會忽略所有表單元素的 valuecheckedselected 特性的初始值而總是將 Vue 實例的數據作爲數據來源。你應該通過 JavaScript 在組件的 data 選項中聲明初始值。

對於需要使用輸入法 (如中文、日文、韓文等) 的語言,你會發現 v-model 不會在輸入法組合文字過程中得到更新。如果你也想處理這個過程,請使用 input 事件。

文本
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
多行文本
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

在文本區域插值 (<textarea></textarea>) 並不會生效,應用 v-model 來代替。

複選框

單個複選框,綁定到布爾值:

<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

多個複選框,綁定到同一個數組:

<div id='example-3'>
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames">
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  <label for="mike">Mike</label>
  <br>
  <span>Checked names: {{ checkedNames }}</span>
</div>
new Vue({
  el: '#example-3',
  data: {
    checkedNames: []
  }
})
單選按鈕
<div id="example-4">
  <input type="radio" id="one" value="One" v-model="picked">
  <label for="one">One</label>
  <br>
  <input type="radio" id="two" value="Two" v-model="picked">
  <label for="two">Two</label>
  <br>
  <span>Picked: {{ picked }}</span>
</div>
new Vue({
  el: '#example-4',
  data: {
    picked: ''
  }
})
選擇框

單選時:

<div id="example-5">
  <select v-model="selected">
    <option disabled value="">請選擇</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>
new Vue({
  el: '...',
  data: {
    selected: ''
  }
})

如果 v-model 表達式的初始值未能匹配任何選項,<select> 元素將被渲染爲“未選中”狀態。在 iOS 中,這會使用戶無法選擇第一個選項。因爲這樣的情況下,iOS 不會觸發 change 事件。因此,更推薦像上面這樣提供一個值爲空的禁用選項。

多選時 (綁定到一個數組):

<div id="example-6">
  <select v-model="selected" multiple style="width: 50px;">
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <br>
  <span>Selected: {{ selected }}</span>
</div>
new Vue({
  el: '#example-6',
  data: {
    selected: []
  }
})

v-for 渲染的動態選項:

<select v-model="selected">
  <option v-for="option in options" v-bind:value="option.value">
    {{ option.text }}
  </option>
</select>
<span>Selected: {{ selected }}</span>
new Vue({
  el: '...',
  data: {
    selected: 'A',
    options: [
      { text: 'One', value: 'A' },
      { text: 'Two', value: 'B' },
      { text: 'Three', value: 'C' }
    ]
  }
})
值綁定

對於單選按鈕,複選框及選擇框的選項,v-model 綁定的值通常是靜態字符串 (對於複選框也可以是布爾值):

<!-- 當選中時,`picked` 爲字符串 "a" -->
<input type="radio" v-model="picked" value="a">

<!-- `toggle` 爲 true 或 false -->
<input type="checkbox" v-model="toggle">

<!-- 當選中時,`selected` 爲字符串 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

但是有時我們可能想把值綁定到 Vue 實例的一個動態屬性上,這時可以用 v-bind 實現,並且這個屬性的值可以不是字符串。

複選框
<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no"
>
// 當選中時
vm.toggle === 'yes'
// 當沒有選中時
vm.toggle === 'no'

這裏的 true-valuefalse-value 特性並不會影響輸入控件的 value 特性,因爲瀏覽器在提交表單時並不會包含未被選中的複選框。如果要確保表單中這兩個值中的一個能夠被提交,(比如“yes”或“no”),請換用單選按鈕。

單選按鈕
<input type="radio" v-model="pick" v-bind:value="a">
// 當選中時
vm.pick === vm.a
選擇框的選項
<select v-model="selected">
    <!-- 內聯對象字面量 -->
  <option v-bind:value="{ number: 123 }">123</option>
</select>
// 當選中時
typeof vm.selected // => 'object'
vm.selected.number // => 123
修飾符
.lazy

在默認情況下,v-model 在每次 input 事件觸發後將輸入框的值與數據進行同步 (除了上述輸入法組合文字時)。你可以添加 lazy 修飾符,從而轉變爲使用 change 事件進行同步:

<!-- 在“change”時而非“input”時更新 -->
<input v-model.lazy="msg" >
.number

如果想自動將用戶的輸入值轉爲數值類型,可以給 v-model 添加 number 修飾符:

<input v-model.number="age" type="number">

這通常很有用,因爲即使在 type="number" 時,HTML 輸入元素的值也總會返回字符串。

.trim

如果要自動過濾用戶輸入的首尾空白字符,可以給 v-model 添加 trim 修飾符:

<input v-model.trim="msg">
在組件上使用 v-model

如果你還不熟悉 Vue 的組件,可以暫且跳過這裏。

HTML 原生的輸入元素類型並不總能滿足需求。幸好,Vue 的組件系統允許你創建具有完全自定義行爲且可複用的輸入組件。這些輸入組件甚至可以和 v-model 一起使用!要了解更多,請參閱組件指南中的自定義輸入組件。

組件

什麼是組件?

組件 (Component) 是 Vue.js 最強大的功能之一。組件可以擴展 HTML 元素,封裝可重用的代碼。在較高層面上,組件是自定義元素,Vue.js 的編譯器爲它添加特殊功能。在有些情況下,組件也可以表現爲用 is 特性進行了擴展的原生 HTML 元素。

所有的 Vue 組件同時也都是 Vue 的實例,所以可接受相同的選項對象 (除了一些根級特有的選項) 並提供相同的生命週期鉤子。

使用組件
全局註冊

我們已經知道,可以通過以下方式創建一個 Vue 實例:

new Vue({
  el: '#some-element',
  // 選項
})

要註冊一個全局組件,可以使用 Vue.component(tagName, options)。例如:

Vue.component('my-component', {
  // 選項
})

請注意,對於自定義標籤的命名 Vue.js 不強制遵循 W3C 規則 (小寫,並且包含一個短槓),儘管這被認爲是最佳實踐。

組件在註冊之後,便可以作爲自定義元素 <my-component></my-component> 在一個實例的模板中使用。注意確保在初始化根實例之前註冊組件:

<div id="example">
  <my-component></my-component>
</div>
// 註冊
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})

// 創建根實例
new Vue({
  el: '#example'
})

渲染爲:

<div id="example">
  <div>A custom component!</div>
</div>
局部註冊

你不必把每個組件都註冊到全局。你可以通過某個 Vue 實例/組件的實例選項 components 註冊僅在其作用域中可用的組件:

var Child = {
  template: '<div>A custom component!</div>'
}

new Vue({
  // ...
  components: {
    // <my-component> 將只在父組件模板中可用
    'my-component': Child
  }
})

這種封裝也適用於其它可註冊的 Vue 功能,比如指令。

DOM 模板解析注意事項

當使用 DOM 作爲模板時 (例如,使用 el 選項來把 Vue 實例掛載到一個已有內容的元素上),你會受到 HTML 本身的一些限制,因爲 Vue 只有在瀏覽器解析、規範化模板之後才能獲取其內容。尤其要注意,像 <ul><ol><table><select> 這樣的元素裏允許包含的元素有限制,而另一些像 <option> 這樣的元素只能出現在某些特定元素的內部。

在自定義組件中使用這些受限制的元素時會導致一些問題,例如:

<table>
  <my-row>...</my-row>
</table>

自定義組件 <my-row> 會被當作無效的內容,因此會導致錯誤的渲染結果。變通的方案是使用特殊的 is 特性:

<table>
  <tr is="my-row"></tr>
</table>

應當注意,如果使用來自以下來源之一的字符串模板,則沒有這些限制:

  • <script type="text/x-template">
  • JavaScript 內聯模板字符串
  • .vue 組件

因此,請儘可能使用字符串模板。

data 必須是函數

構造 Vue 實例時傳入的各種選項大多數都可以在組件裏使用。只有一個例外:data 必須是函數。實際上,如果你這麼做:

Vue.component('my-component', {
  template: '<span>{{ message }}</span>',
  data: {
    message: 'hello'
  }
})

那麼 Vue 會停止運行,並在控制檯發出警告,告訴你在組件實例中 data 必須是一個函數。但理解這種規則爲何存在也是很有益處的,所以讓我們先作個弊:

<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
var data = { counter: 0 }

Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // 技術上 data 的確是一個函數了,因此 Vue 不會警告,
  // 但是我們卻給每個組件實例返回了同一個對象的引用
  data: function () {
    return data
  }
})

new Vue({
  el: '#example-2'
})

由於這三個組件實例共享了同一個 data 對象,因此遞增一個 counter 會影響所有組件!這就錯了。我們可以通過爲每個組件返回全新的數據對象來修復這個問題:

data: function () {
  return {
    counter: 0
  }
}

現在每個 counter 都有它自己內部的狀態了。

組件組合

組件設計初衷就是要配合使用的,最常見的就是形成父子組件的關係:組件 A 在它的模板中使用了組件 B。它們之間必然需要相互通信:父組件可能要給子組件下發數據,子組件則可能要將它內部發生的事情告知父組件。然而,通過一個良好定義的接口來儘可能將父子組件解耦也是很重要的。這保證了每個組件的代碼可以在相對隔離的環境中書寫和理解,從而提高了其可維護性和複用性。

在 Vue 中,父子組件的關係可以總結爲 prop 向下傳遞事件向上傳遞。父組件通過 prop 給子組件下發數據,子組件通過事件給父組件發送消息。看看它們是怎麼工作的。

Prop
使用 Prop 傳遞數據

組件實例的作用域是孤立的。這意味着不能 (也不應該) 在子組件的模板內直接引用父組件的數據。父組件的數據需要通過 prop 才能下發到子組件中。

子組件要顯式地用 props 選項聲明它預期的數據:

Vue.component('child', {
  // 聲明 props
  props: ['message'],
  // 就像 data 一樣,prop 也可以在模板中使用
  // 同樣也可以在 vm 實例中通過 this.message 來使用
  template: '<span>{{ message }}</span>'
})

然後我們可以這樣向它傳入一個普通字符串:

<child message="hello!"></child>
camelCase vs. kebab-case

HTML 特性是不區分大小寫的。所以,當使用的不是字符串模板時,camelCase (駝峯式命名) 的 prop 需要轉換爲相對應的 kebab-case (短橫線分隔式命名):

Vue.component('child', {
  // 在 JavaScript 中使用 camelCase
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- 在 HTML 中使用 kebab-case -->
<child my-message="hello!"></child>

如果你使用字符串模板,則沒有這些限制。

動態 Prop

與綁定到任何普通的 HTML 特性相類似,我們可以用 v-bind 來動態地將 prop 綁定到父組件的數據。每當父組件的數據變化時,該變化也會傳導給子組件:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

你也可以使用 v-bind 的縮寫語法:

<child :my-message="parentMsg"></child>

如果你想把一個對象的所有屬性作爲 prop 進行傳遞,可以使用不帶任何參數的 v-bind (即用 v-bind 而不是 v-bind:prop-name)。例如,已知一個 todo 對象:

todo: {
  text: 'Learn Vue',
  isComplete: false
}

然後:

<todo-item v-bind="todo"></todo-item>

將等價於:

<todo-item
  v-bind:text="todo.text"
  v-bind:is-complete="todo.isComplete"
></todo-item>
字面量語法 vs 動態語法

初學者常犯的一個錯誤是使用字面量語法傳遞數值:

<!-- 傳遞了一個字符串 "1" -->
<comp some-prop="1"></comp>

因爲它是一個字面量 prop,它的值是字符串 "1" 而不是一個數值。如果想傳遞一個真正的 JavaScript 數值,則需要使用 v-bind,從而讓它的值被當作 JavaScript 表達式計算:

<!-- 傳遞真正的數值 -->
<comp v-bind:some-prop="1"></comp>
單向數據流

Prop 是單向綁定的:當父組件的屬性變化時,將傳導給子組件,但是反過來不會。這是爲了防止子組件無意間修改了父組件的狀態,來避免應用的數據流變得難以理解。

另外,每次父組件更新時,子組件的所有 prop 都會更新爲最新值。這意味着你不應該在子組件內部改變 prop。如果你這麼做了,Vue 會在控制檯給出警告。

在兩種情況下,我們很容易忍不住想去修改 prop 中數據:

  1. Prop 作爲初始值傳入後,子組件想把它當作局部數據來用;
  2. Prop 作爲原始數據傳入,由子組件處理成其它數據輸出。

對這兩種情況,正確的應對方式是:

  1. 定義一個局部變量,並用 prop 的值初始化它:
props: ['initialCounter'],
data: function () {
  return { counter: this.initialCounter }
}
  1. 定義一個計算屬性,處理 prop 的值並返回:
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

注意在 JavaScript 中對象和數組是引用類型,指向同一個內存空間,如果 prop 是一個對象或數組,在子組件內部改變它會影響父組件的狀態。

Prop 驗證

我們可以爲組件的 prop 指定驗證規則。如果傳入的數據不符合要求,Vue 會發出警告。這對於開發給他人使用的組件非常有用。

要指定驗證規則,需要用對象的形式來定義 prop,而不能用字符串數組:

Vue.component('example', {
  props: {
    // 基礎類型檢測 (`null` 指允許任何類型)
    propA: Number,
    // 可能是多種類型
    propB: [String, Number],
    // 必傳且是字符串
    propC: {
      type: String,
      required: true
    },
    // 數值且有默認值
    propD: {
      type: Number,
      default: 100
    },
    // 數組/對象的默認值應當由一個工廠函數返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定義驗證函數
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

type 可以是下面原生構造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array
  • Symbol

type 也可以是一個自定義構造器函數,使用 instanceof 檢測。

當 prop 驗證失敗,Vue 會拋出警告 (如果使用的是開發版本)。注意 prop 會在組件實例創建之前進行校驗,所以在 defaultvalidator 函數裏,諸如 datacomputedmethods 等實例屬性還無法使用。

非 Prop 特性

所謂非 prop 特性,就是指它可以直接傳入組件,而不需要定義相應的 prop。

儘管爲組件定義明確的 prop 是推薦的傳參方式,組件的作者卻並不總能預見到組件被使用的場景。所以,組件可以接收任意傳入的特性,這些特性都會被添加到組件的根元素上。

例如,假設我們使用了第三方組件 bs-date-input,它包含一個 Bootstrap 插件,該插件需要在 input 上添加 data-3d-date-picker 這個特性。這時可以把特性直接添加到組件上 (不需要事先定義 prop):

<bs-date-input data-3d-date-picker="true"></bs-date-input>

添加屬性 data-3d-date-picker="true" 之後,它會被自動添加到 bs-date-input 的根元素上。

替換/合併現有的特性

假設這是 bs-date-input 的模板:

<input type="date" class="form-control">

爲了給該日期選擇器插件增加一個特殊的主題,我們可能需要增加一個特殊的 class,比如:

<bs-date-input
  data-3d-date-picker="true"
  class="date-picker-theme-dark"
></bs-date-input>

在這個例子當中,我們定義了兩個不同的 class 值:

  • form-control,來自組件自身的模板
  • date-picker-theme-dark,來自父組件

對於多數特性來說,傳遞給組件的值會覆蓋組件本身設定的值。即例如傳遞 type="large" 將會覆蓋 type="date" 且有可能破壞該組件!所幸我們對待 classstyle 特性會更聰明一些,這兩個特性的值都會做合併 (merge) 操作,讓最終生成的值爲:form-control date-picker-theme-dark

自定義事件

我們知道,父組件使用 prop 傳遞數據給子組件。但子組件怎麼跟父組件通信呢?這個時候 Vue 的自定義事件系統就派得上用場了。

使用 v-on 綁定自定義事件

每個 Vue 實例都實現了事件接口,即:

  • 使用 $on(eventName) 監聽事件
  • 使用 $emit(eventName) 觸發事件

Vue 的事件系統與瀏覽器的 EventTarget API 有所不同。儘管它們的運行起來類似,但是 $on$emit 並不是addEventListenerdispatchEvent 的別名。

另外,父組件可以在使用子組件的地方直接用 v-on 來監聽子組件觸發的事件。

不能用 $on 監聽子組件釋放的事件,而必須在模板裏直接用 v-on 綁定,參見下面的例子。

下面是一個例子:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})

new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

在本例中,子組件已經和它外部完全解耦了。它所做的只是報告自己的內部事件,因爲父組件可能會關心這些事件。請注意這一點很重要。

給組件綁定原生事件

有時候,你可能想在某個組件的根元素上監聽一個原生事件。可以使用 v-on 的修飾符 .native。例如:

<my-component v-on:click.native="doTheThing"></my-component>
.sync 修飾符

2.3.0+

在一些情況下,我們可能會需要對一個 prop 進行“雙向綁定”。事實上,這正是 Vue 1.x 中的 .sync 修飾符所提供的功能。當一個子組件改變了一個帶 .sync 的 prop 的值時,這個變化也會同步到父組件中所綁定的值。這很方便,但也會導致問題,因爲它破壞了單向數據流。由於子組件改變 prop 的代碼和普通的狀態改動代碼毫無區別,當光看子組件的代碼時,你完全不知道它何時悄悄地改變了父組件的狀態。這在 debug 複雜結構的應用時會帶來很高的維護成本。

上面所說的正是我們在 2.0 中移除 .sync 的理由。但是在 2.0 發佈之後的實際應用中,我們發現 .sync 還是有其適用之處,比如在開發可複用的組件庫時。我們需要做的只是讓子組件改變父組件狀態的代碼更容易被區分

如下代碼

<comp :foo.sync="bar"></comp>

會被擴展爲:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

當子組件需要更新 foo 的值時,它需要顯式地觸發一個更新事件:

this.$emit('update:foo', newValue)
使用自定義事件的表單輸入組件

自定義事件可以用來創建自定義的表單輸入組件,使用 v-model 來進行數據雙向綁定。要牢記:

<input v-model="something">

這不過是以下示例的語法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

所以在組件中使用時,它相當於下面的簡寫:

<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>

所以要讓組件的 v-model 生效,它應該 (從 2.2.0 起是可配置的):

  • 接受一個 value prop
  • 在有新的值時觸發 input 事件並將新值作爲參數

我們來看一個非常簡單的貨幣輸入的自定義控件:

<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制
    updateValue: function (value) {
      var formattedValue = value
        // 刪除兩側的空格符
        .trim()
        // 保留 2 位小數
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // 如果值尚不合規,則手動覆蓋爲合規的值
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通過 input 事件帶出數值
      this.$emit('input', Number(formattedValue))
    }
  }
})

當然,上面的例子還是比較初級的。比如,用戶輸入多個小數點或句號也是允許的,好惡心吧!因此我們需要一個複雜一些的例子,下面是一個更加完善的貨幣過濾器:

<div id="app">
  <currency-input 
    label="Price" 
    v-model="price"
  ></currency-input>
  <currency-input 
    label="Shipping" 
    v-model="shipping"
  ></currency-input>
  <currency-input 
    label="Handling" 
    v-model="handling"
  ></currency-input>
  <currency-input 
    label="Discount" 
    v-model="discount"
  ></currency-input>

  <p>Total: ${{ total }}</p>
</div>
Vue.component('currency-input', {
  template: '\
    <div>\
      <label v-if="label">{{ label }}</label>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
        v-on:focus="selectAll"\
        v-on:blur="formatValue"\
      >\
    </div>\
  ',
  props: {
    value: {
      type: Number,
      default: 0
    },
    label: {
      type: String,
      default: ''
    }
  },
  mounted: function () {
    this.formatValue()
  },
  methods: {
    updateValue: function (value) {
      var result = currencyValidator.parse(value, this.value)
      if (result.warning) {
        this.$refs.input.value = result.value
      }
      this.$emit('input', result.value)
    },
    formatValue: function () {
      this.$refs.input.value = currencyValidator.format(this.value)
    },
    selectAll: function (event) {
      // Workaround for Safari bug
      // http://stackoverflow.com/questions/1269722/selecting-text-on-focus-using-jquery-not-working-in-safari-and-chrome
      setTimeout(function () {
        event.target.select()
      }, 0)
    }
  }
})

new Vue({
  el: '#app',
  data: {
    price: 0,
    shipping: 0,
    handling: 0,
    discount: 0
  },
  computed: {
    total: function () {
      return ((
        this.price * 100 + 
        this.shipping * 100 + 
        this.handling * 100 - 
        this.discount * 100
      ) / 100).toFixed(2)
    }
  }
})
自定義組件的 v-model

2.2.0 新增

默認情況下,一個組件的 v-model 會使用 value prop 和 input 事件。但是諸如單選框、複選框之類的輸入類型可能把 value 用作了別的目的。model 選項可以避免這樣的衝突:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 這樣就允許拿 `value` 這個 prop 做其它事了
    value: String
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代碼等價於:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

注意你仍然需要顯式聲明 checked 這個 prop。

非父子組件的通信

有時候,非父子關係的兩個組件之間也需要通信。在簡單的場景下,可以使用一個空的 Vue 實例作爲事件總線:

var bus = new Vue()
// 觸發組件 A 中的事件
bus.$emit('id-selected', 1)
// 在組件 B 創建的鉤子中監聽事件
bus.$on('id-selected', function (id) {
  // ...
})

在複雜的情況下,我們應該考慮使用專門的狀態管理模式。

使用插槽分發內容

在使用組件時,我們常常要像這樣組合它們:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

注意兩點:

  1. `<app> 組件不知道它會收到什麼內容。這是由使用 <app> 的父組件決定的。
  2. <app> 組件很可能有它自己的模板。

爲了讓組件可以組合,我們需要一種方式來混合父組件的內容與子組件自己的模板。這個過程被稱爲內容分發 (即 Angular 用戶熟知的“transclusion”)。Vue.js 實現了一個內容分發 API,參照了當前 Web Components 規範草案,使用特殊的 <slot> 元素作爲原始內容的插槽。

編譯作用域

在深入內容分發 API 之前,我們先明確內容在哪個作用域裏編譯。假定模板爲:

<child-component>
  {{ message }}
</child-component>

message 應該綁定到父組件的數據,還是綁定到子組件的數據?答案是父組件。組件作用域簡單地說是:

父組件模板的內容在父組件作用域內編譯;子組件模板的內容在子組件作用域內編譯。

一個常見錯誤是試圖在父組件模板內將一個指令綁定到子組件的屬性/方法:

<!-- 無效 -->
<child-component v-show="someChildProperty"></child-component>

假定 someChildProperty 是子組件的屬性,上例不會如預期那樣工作。父組件模板並不感知子組件的狀態。

如果要綁定子組件作用域內的指令到一個組件的根節點,你應當在子組件自己的模板裏做:

Vue.component('child-component', {
  // 有效,因爲是在正確的作用域內
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

類似地,被分發的內容會在父作用域內編譯。

單個插槽

除非子組件模板包含至少一個 <slot> 插口,否則父組件的內容將會被丟棄。當子組件模板只有一個沒有屬性的插槽時,父組件傳入的整個內容片段將插入到插槽所在的 DOM 位置,並替換掉插槽標籤本身。

最初在 <slot> 標籤中的任何內容都被視爲備用內容。備用內容在子組件的作用域內編譯,並且只有在宿主元素爲空,且沒有要插入的內容時才顯示備用內容。

假定 my-component 組件有如下模板:

<div>
  <h2>我是子組件的標題</h2>
  <slot>
    只有在沒有要分發的內容時纔會顯示。
  </slot>
</div>

父組件模板:

<div>
  <h1>我是父組件的標題</h1>
  <my-component>
    <p>這是一些初始內容</p>
    <p>這是更多的初始內容</p>
  </my-component>
</div>

渲染結果:

<div>
  <h1>我是父組件的標題</h1>
  <div>
    <h2>我是子組件的標題</h2>
    <p>這是一些初始內容</p>
    <p>這是更多的初始內容</p>
  </div>
</div>
具名插槽

<slot> 元素可以用一個特殊的特性 name 來進一步配置如何分發內容。多個插槽可以有不同的名字。具名插槽將匹配內容片段中有對應 slot 特性的元素。

仍然可以有一個匿名插槽,它是默認插槽,作爲找不到匹配的內容片段的備用插槽。如果沒有默認插槽,這些找不到匹配的內容片段將被拋棄。

例如,假定我們有一個 app-layout 組件,它的模板爲:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

父組件模板:

<app-layout>
  <h1 slot="header">這裏可能是一個頁面標題</h1>

  <p>主要內容的一個段落。</p>
  <p>另一個主要段落。</p>

  <p slot="footer">這裏有一些聯繫信息</p>
</app-layout>

渲染結果爲:

<div class="container">
  <header>
    <h1>這裏可能是一個頁面標題</h1>
  </header>
  <main>
    <p>主要內容的一個段落。</p>
    <p>另一個主要段落。</p>
  </main>
  <footer>
    <p>這裏有一些聯繫信息</p>
  </footer>
</div>

在設計組合使用的組件時,內容分發 API 是非常有用的機制。

作用域插槽

2.1.0 新增

作用域插槽是一種特殊類型的插槽,用作一個 (能被傳遞數據的) 可重用模板,來代替已經渲染好的元素。

在子組件中,只需將數據傳遞到插槽,就像你將 prop 傳遞給組件一樣:

<div class="child">
  <slot text="hello from child"></slot>
</div>

在父級中,具有特殊特性 slot-scope<template> 元素必須存在,表示它是作用域插槽的模板。slot-scope 的值將被用作一個臨時變量名,此變量接收從子組件傳遞過來的 prop 對象:

<div class="parent">
  <child>
    <template slot-scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

如果我們渲染上述模板,得到的輸出會是:

<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>

在 2.5.0+,slot-scope 能被用在任意元素或組件中而不再侷限於 <template>

作用域插槽更典型的用例是在列表組件中,允許使用者自定義如何渲染列表的每一項:

<my-awesome-list :items="items">
  <!-- 作用域插槽也可以是具名的 -->
  <li
    slot="item"
    slot-scope="props"
    class="my-fancy-item">
    {{ props.text }}
  </li>
</my-awesome-list>

列表組件的模板:

<ul>
  <slot name="item"
    v-for="item in items"
    :text="item.text">
    <!-- 這裏寫入備用內容 -->
  </slot>
</ul>

解構

slot-scope 的值實際上是一個可以出現在函數簽名參數位置的合法的 JavaScript 表達式。這意味着在受支持的環境 (單文件組件或現代瀏覽器) 中,您還可以在表達式中使用 ES2015 解構:

<child>
  <span slot-scope="{ text }">{{ text }}</span>
</child>
動態組件

通過使用保留的 <component> 元素,並對其 is 特性進行動態綁定,你可以在同一個掛載點動態切換多個組件:

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
<component v-bind:is="currentView">
  <!-- 組件在 vm.currentview 變化時改變! -->
</component>

也可以直接綁定到組件對象上:

var Home = {
  template: '<p>Welcome home!</p>'
}

var vm = new Vue({
  el: '#example',
  data: {
    currentView: Home
  }
})
keep-alive

如果把切換出去的組件保留在內存中,可以保留它的狀態或避免重新渲染。爲此可以添加一個 keep-alive 指令參數:

<keep-alive>
  <component :is="currentView">
    <!-- 非活動組件將被緩存! -->
  </component>
</keep-alive>

API 參考中查看更多 <keep-alive> 的細節。

雜項
編寫可複用組件

在編寫組件時,最好考慮好以後是否要進行復用。一次性組件間有緊密的耦合沒關係,但是可複用組件應當定義一個清晰的公開接口,同時也不要對其使用的外層數據作出任何假設。

Vue 組件的 API 來自三部分——prop、事件和插槽:

  • Prop 允許外部環境傳遞數據給組件;
  • 事件允許從組件內觸發外部環境的副作用;
  • 插槽允許外部環境將額外的內容組合在組件中。

使用 v-bindv-on 的簡寫語法,模板的意圖會更清楚且簡潔:

<my-component
  :foo="baz"
  :bar="qux"
  @event-a="doThis"
  @event-b="doThat"
>
  <img slot="icon" src="...">
  <p slot="main-text">Hello!</p>
</my-component>
子組件引用

儘管有 prop 和事件,但是有時仍然需要在 JavaScript 中直接訪問子組件。爲此可以使用 ref 爲子組件指定一個引用 ID。例如:

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 訪問子組件實例
var child = parent.$refs.profile

refv-for 一起使用時,獲取到的引用會是一個數組,包含和循環數據源對應的子組件。

$refs 只在組件渲染完成後才填充,並且它是非響應式的。它僅僅是一個直接操作子組件的應急方案——應當避免在模板或計算屬性中使用 $refs

異步組件

在大型應用中,我們可能需要將應用拆分爲多個小模塊,按需從服務器下載。爲了進一步簡化,Vue.js 允許將組件定義爲一個工廠函數,異步地解析組件的定義。Vue.js 只在組件需要渲染時觸發工廠函數,並且把結果緩存起來,用於後面的再次渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 將組件定義傳入 resolve 回調函數
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

工廠函數接收一個 resolve 回調,在收到從服務器下載的組件定義時調用。也可以調用 reject(reason) 指示加載失敗。這裏使用 setTimeout 只是爲了演示,實際上如何獲取組件完全由你決定。推薦配合 webpack 的代碼分割功能 來使用:

Vue.component('async-webpack-example', function (resolve) {
  // 這個特殊的 require 語法告訴 webpack
  // 自動將編譯後的代碼分割成不同的塊,
  // 這些塊將通過 Ajax 請求自動下載。
  require(['./my-async-component'], resolve)
})

你可以在工廠函數中返回一個 Promise,所以當使用 webpack 2 + ES2015 的語法時可以這樣:

Vue.component(
  'async-webpack-example',
  // 該 `import` 函數返回一個 `Promise` 對象。
  () => import('./my-async-component')
)

當使用局部註冊時,也可以直接提供一個返回 Promise 的函數:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})

如果你是 Browserify 用戶,可能就無法使用異步組件了,它的作者已經表明 Browserify 將“永遠不會支持異步加載”。Browserify 社區發現了一些解決方法,可能會有助於已存在的複雜應用。對於其他場景,我們推薦使用 webpack,因爲它對異步加載進行了內置、全面的支持。

高級異步組件

2.3.0 新增

自 2.3.0 起,異步組件的工廠函數也可以返回一個如下的對象:

const AsyncComp = () => ({
  // 需要加載的組件。應當是一個 Promise
  component: import('./MyComp.vue'),
  // 加載中應當渲染的組件
  loading: LoadingComp,
  // 出錯時渲染的組件
  error: ErrorComp,
  // 渲染加載中組件前的等待時間。默認:200ms。
  delay: 200,
  // 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity
  timeout: 3000
})

注意,當一個異步組件被作爲 vue-router 的路由組件使用時,這些高級選項都是無效的,因爲在路由切換前就會提前加載所需要的異步組件。另外,如果你要在路由組件中使用上述寫法,需要使用 vue-router 2.4.0 以上的版本。

組件命名約定

當註冊組件 (或者 prop) 時,可以使用 kebab-case (短橫線分隔命名)、camelCase (駝峯式命名) 或 PascalCase (單詞首字母大寫命名)。

// 在組件定義中
components: {
  // 使用 kebab-case 註冊
  'kebab-cased-component': { /* ... */ },
  // 使用 camelCase 註冊
  'camelCasedComponent': { /* ... */ },
  // 使用 PascalCase 註冊
  'PascalCasedComponent': { /* ... */ }
}

在 HTML 模板中,請使用 kebab-case:

<!-- 在 HTML 模板中始終使用 kebab-case -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>

當使用字符串模式時,可以不受 HTML 大小寫不敏感的限制。這意味實際上在模板中,你可以使用下面的方式來引用你的組件:

  • kebab-case
  • camelCase 或 kebab-case (如果組件已經被定義爲 camelCase)
  • kebab-case、camelCase 或 PascalCase (如果組件已經被定義爲 PascalCase)
components: {
  'kebab-cased-component': { /* ... */ },
  camelCasedComponent: { /* ... */ },
  PascalCasedComponent: { /* ... */ }
}
<kebab-cased-component></kebab-cased-component>

<camel-cased-component></camel-cased-component>
<camelCasedComponent></camelCasedComponent>

<pascal-cased-component></pascal-cased-component>
<pascalCasedComponent></pascalCasedComponent>
<PascalCasedComponent></PascalCasedComponent>

這意味着 PascalCase 是最通用的聲明約定而 kebab-case 是最通用的使用約定

如果組件未經 slot 元素傳入內容,你甚至可以在組件名後使用 / 使其自閉合:

<my-component/>

當然,這只在字符串模板中有效。因爲自閉的自定義元素是無效的 HTML,瀏覽器原生的解析器也無法識別它。

遞歸組件

組件在它的模板內可以遞歸地調用自己。不過,只有當它有 name 選項時纔可以這麼做:

name: 'unique-name-of-my-component'

當你利用 Vue.component 全局註冊了一個組件,全局的 ID 會被自動設置爲組件的 name

Vue.component('unique-name-of-my-component', {
  // ...
})

如果稍有不慎,遞歸組件可能導致死循環:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

上面組件會導致一個“max stack size exceeded”錯誤,所以要確保遞歸調用有終止條件 (比如遞歸調用時使用 v-if 並最終解析爲 false)。

組件間的循環引用

假設你正在構建一個文件目錄樹,像在 Finder 或資源管理器中。你可能有一個 tree-folder 組件:

<p>
  <span>{{ folder.name }}</span>
  <tree-folder-contents :children="folder.children"/>
</p>

以及一個 tree-folder-contents 組件:

<ul>
  <li v-for="child in children">
    <tree-folder v-if="child.children" :folder="child"/>
    <span v-else>{{ child.name }}</span>
  </li>
</ul>

當你仔細看時,會發現在渲染樹上這兩個組件同時爲對方的父節點和子節點——這是矛盾的!當使用 Vue.component 將這兩個組件註冊爲全局組件的時候,框架會自動爲你解決這個矛盾。如果你已經是這樣做的,就跳過下面這段吧。

然而,如果你使用諸如 webpack 或者 Browserify 之類的模塊化管理工具來 require/import 組件的話,就會報錯了:

Failed to mount component: template or render function not defined.

爲了解釋爲什麼會報錯,簡單的將上面兩個組件稱爲 A 和 B。模塊系統看到它需要 A,但是首先 A 需要 B,但是 B 需要 A,而 A 需要 B,循環往復。因爲不知道到底應該先解析哪個,所以將會陷入無限循環。要解決這個問題,我們需要在其中一個組件中告訴模塊化管理系統:“A 雖然最後會用到 B,但是不需要優先導入 B”。

在我們的例子中,可以選擇讓 tree-folder 組件中來做這件事。我們知道引起矛盾的子組件是 tree-folder-contents,所以我們要等到 beforeCreate 生命週期鉤子中才去註冊它:

beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

問題解決了!

內聯模板

如果子組件有 inline-template 特性,組件將把它的內容當作它的模板,而不是把它當作分發內容。這讓模板編寫起來更靈活。

<my-component inline-template>
  <div>
    <p>這些將作爲組件自身的模板。</p>
    <p>而非父組件透傳進來的內容。</p>
  </div>
</my-component>

但是 inline-template 讓模板的作用域難以理解。使用 template 選項在組件內定義模板或者在 .vue 文件中使用 template 元素纔是最佳實踐。

X-Template

另一種定義模板的方式是在 JavaScript 標籤裏使用 text/x-template 類型,並且指定一個 id。例如:

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

這在有很多大模板的演示應用或者特別小的應用中可能有用,其它場合應該避免使用,因爲這將模板和組件的其它定義分離了。

對低開銷的靜態組件使用 v-once

儘管在 Vue 中渲染 HTML 很快,不過當組件中包含大量靜態內容時,可以考慮使用 v-once 將渲染結果緩存起來,就像這樣:

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Terms of Service</h1>\
      ...很多靜態內容...\
    </div>\
  '
})

過渡 & 動畫

進入/離開 & 列表過渡

概述

Vue 在插入、更新或者移除 DOM 時,提供多種不同方式的應用過渡效果。
包括以下工具:

  • 在 CSS 過渡和動畫中自動應用 class
  • 可以配合使用第三方 CSS 動畫庫,如 Animate.css
  • 在過渡鉤子函數中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 動畫庫,如 Velocity.js

在這裏,我們只會講到進入、離開和列表的過渡,你也可以看下一節的 管理過渡狀態。

單元素/組件的過渡

Vue 提供了 transition 的封裝組件,在下列情形中,可以給任何元素和組件添加 entering/leaving 過渡

  • 條件渲染 (使用 v-if)
  • 條件展示 (使用 v-show)
  • 動態組件
  • 組件根節點

這裏是一個典型的例子:

<div id="demo">
  <button v-on:click="show = !show">
    Toggle
  </button>
  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
new Vue({
  el: '#demo',
  data: {
    show: true
  }
})
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}

當插入或刪除包含在 transition 組件中的元素時,Vue 將會做以下處理:

  1. 自動嗅探目標元素是否應用了 CSS 過渡或動畫,如果是,在恰當的時機添加/刪除 CSS 類名。
  2. 如果過渡組件提供了 JavaScript 鉤子函數,這些鉤子函數將在恰當的時機被調用。
  3. 如果沒有找到 JavaScript 鉤子並且也沒有檢測到 CSS 過渡/動畫,DOM 操作 (插入/刪除) 在下一幀中立即執行。(注意:此指瀏覽器逐幀動畫機制,和 Vue 的 nextTick 概念不同)
過渡的類名

在進入/離開的過渡中,會有 6 個 class 切換。

  1. v-enter:定義進入過渡的開始狀態。在元素被插入時生效,在下一個幀移除。
  2. v-enter-active:定義過渡的狀態。在元素整個過渡過程中作用,在元素被插入時生效,在 transition/animation 完成之後移除。這個類可以被用來定義過渡的過程時間,延遲和曲線函數。
  3. v-enter-to: 2.1.8版及以上 定義進入過渡的結束狀態。在元素被插入一幀後生效 (與此同時 v-enter 被刪除),在 transition/animation 完成之後移除。
  4. v-leave: 定義離開過渡的開始狀態。在離開過渡被觸發時生效,在下一個幀移除。
  5. v-leave-active:定義過渡的狀態。在元素整個過渡過程中作用,在離開過渡被觸發後立即生效,在 transition/animation 完成之後移除。這個類可以被用來定義過渡的過程時間,延遲和曲線函數。
  6. v-leave-to: 2.1.8版及以上 定義離開過渡的結束狀態。在離開過渡被觸發一幀後生效 (與此同時 v-leave 被刪除),在 transition/animation 完成之後移除。

對於這些在 enter/leave 過渡中切換的類名,v- 是這些類名的前綴。使用 <transition name="my-transition"> 可以重置前綴,比如 v-enter 替換爲 my-transition-enter

v-enter-activev-leave-active 可以控制 進入/離開 過渡的不同階段,在下面章節會有個示例說明。

CSS 過渡

常用的過渡都是使用 CSS 過渡。

下面是一個簡單例子:

<div id="example-1">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition name="slide-fade">
    <p v-if="show">hello</p>
  </transition>
</div>
new Vue({
  el: '#example-1',
  data: {
    show: true
  }
})
/* 可以設置不同的進入和離開動畫 */
/* 設置持續時間和動畫函數 */
.slide-fade-enter-active {
  transition: all .3s ease;
}
.slide-fade-leave-active {
  transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
  transform: translateX(10px);
  opacity: 0;
}
CSS 動畫

CSS 動畫用法同 CSS 過渡,區別是在動畫中 v-enter 類名在節點插入 DOM 後不會立即刪除,而是在 animationend 事件觸發時刪除。

示例:(省略了兼容性前綴)

<div id="example-2">
  <button @click="show = !show">Toggle show</button>
  <transition name="bounce">
    <p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.</p>
  </transition>
</div>
new Vue({
  el: '#example-2',
  data: {
    show: true
  }
})
.bounce-enter-active {
  animation: bounce-in .5s;
}
.bounce-leave-active {
  animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
自定義過渡的類名

我們可以通過以下特性來自定義過渡類名:

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)

他們的優先級高於普通的類名,這對於 Vue 的過渡系統和其他第三方 CSS 動畫庫,如 Animate.css 結合使用十分有用。

示例:

<link href="https://cdn.jsdelivr.net/npm/[email protected]" rel="stylesheet" type="text/css">

<div id="example-3">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition
    name="custom-classes-transition"
    enter-active-class="animated tada"
    leave-active-class="animated bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>
new Vue({
  el: '#example-3',
  data: {
    show: true
  }
})
同時使用過渡和動畫

Vue 爲了知道過渡的完成,必須設置相應的事件監聽器。它可以是 transitionendanimationend ,這取決於給元素應用的 CSS 規則。如果你使用其中任何一種,Vue 能自動識別類型並設置監聽。

但是,在一些場景中,你需要給同一個元素同時設置兩種過渡動效,比如 animation 很快的被觸發並完成了,而 transition 效果還沒結束。在這種情況中,你就需要使用 type 特性並設置 animationtransition 來明確聲明你需要 Vue 監聽的類型。

顯性的過渡持續時間

2.2.0 新增

在很多情況下,Vue 可以自動得出過渡效果的完成時機。默認情況下,Vue 會等待其在過渡效果的根元素的第一個 transitionendanimationend 事件。然而也可以不這樣設定——比如,我們可以擁有一個精心編排的一序列過渡效果,其中一些嵌套的內部元素相比於過渡效果的根元素有延遲的或更長的過渡效果。

在這種情況下你可以用 <transition> 組件上的 duration 屬性定製一個顯性的過渡持續時間 (以毫秒計):

<transition :duration="1000">...</transition>

你也可以定製進入和移出的持續時間:

<transition :duration="{ enter: 500, leave: 800 }">...</transition>
JavaScript 鉤子

可以在屬性中聲明 JavaScript 鉤子

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <!-- ... -->
</transition>
// ...
methods: {
  // --------
  // 進入中
  // --------

  beforeEnter: function (el) {
    // ...
  },
  // 此回調函數是可選項的設置
  // 與 CSS 結合時使用
  enter: function (el, done) {
    // ...
    done()
  },
  afterEnter: function (el) {
    // ...
  },
  enterCancelled: function (el) {
    // ...
  },

  // --------
  // 離開時
  // --------

  beforeLeave: function (el) {
    // ...
  },
  // 此回調函數是可選項的設置
  // 與 CSS 結合時使用
  leave: function (el, done) {
    // ...
    done()
  },
  afterLeave: function (el) {
    // ...
  },
  // leaveCancelled 只用於 v-show 中
  leaveCancelled: function (el) {
    // ...
  }
}

這些鉤子函數可以結合 CSS transitions/animations 使用,也可以單獨使用。

當只用 JavaScript 過渡的時候, enterleave 中,回調函數 done 是必須的 。否則,它們會被同步調用,過渡會立即完成。

推薦對於僅使用 JavaScript 過渡的元素添加 v-bind:css="false",Vue 會跳過 CSS 的檢測。這也可以避免過渡過程中 CSS 的影響。

一個使用 Velocity.js 的簡單例子:

<!--
Velocity works very much like jQuery.animate and is
a great option for JavaScript animations
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="example-4">
  <button @click="show = !show">
    Toggle
  </button>
  <transition
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    v-bind:css="false"
  >
    <p v-if="show">
      Demo
    </p>
  </transition>
</div>
new Vue({
  el: '#example-4',
  data: {
    show: false
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
      el.style.transformOrigin = 'left'
    },
    enter: function (el, done) {
      Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
      Velocity(el, { fontSize: '1em' }, { complete: done })
    },
    leave: function (el, done) {
      Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
      Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
      Velocity(el, {
        rotateZ: '45deg',
        translateY: '30px',
        translateX: '30px',
        opacity: 0
      }, { complete: done })
    }
  }
})
初始渲染的過渡

可以通過 appear 特性設置節點在初始渲染的過渡

<transition appear>
  <!-- ... -->
</transition>

這裏默認和進入/離開過渡一樣,同樣也可以自定義 CSS 類名。

<transition
  appear
  appear-class="custom-appear-class"
  appear-to-class="custom-appear-to-class" (2.1.8+)
  appear-active-class="custom-appear-active-class"
>
  <!-- ... -->
</transition>

自定義 JavaScript 鉤子:

<transition
  appear
  v-on:before-appear="customBeforeAppearHook"
  v-on:appear="customAppearHook"
  v-on:after-appear="customAfterAppearHook"
  v-on:appear-cancelled="customAppearCancelledHook"
>
  <!-- ... -->
</transition>
多個元素的過渡

我們之後討論多個組件的過渡,對於原生標籤可以使用 v-if/v-else 。最常見的多標籤過渡是一個列表和描述這個列表爲空消息的元素:

<transition>
  <table v-if="items.length > 0">
    <!-- ... -->
  </table>
  <p v-else>Sorry, no items found.</p>
</transition>

可以這樣使用,但是有一點需要注意:

當有相同標籤名的元素切換時,需要通過 key 特性設置唯一的值來標記以讓 Vue 區分它們,否則 Vue 爲了效率只會替換相同標籤內部的內容。即使在技術上沒有必要,給在 <transition> 組件中的多個元素設置 key 是一個更好的實踐

示例:

<transition>
  <button v-if="isEditing" key="save">
    Save
  </button>
  <button v-else key="edit">
    Edit
  </button>
</transition>

在一些場景中,也可以通過給同一個元素的 key 特性設置不同的狀態來代替 v-ifv-else,上面的例子可以重寫爲:

<transition>
  <button v-bind:key="isEditing">
    {{ isEditing ? 'Save' : 'Edit' }}
  </button>
</transition>

使用多個 v-if 的多個元素的過渡可以重寫爲綁定了動態屬性的單個元素過渡。例如:

<transition>
  <button v-if="docState === 'saved'" key="saved">
    Edit
  </button>
  <button v-if="docState === 'edited'" key="edited">
    Save
  </button>
  <button v-if="docState === 'editing'" key="editing">
    Cancel
  </button>
</transition>

可以重寫爲:

<transition>
  <button v-bind:key="docState">
    {{ buttonMessage }}
  </button>
</transition>
// ...
computed: {
  buttonMessage: function () {
    switch (this.docState) {
      case 'saved': return 'Edit'
      case 'edited': return 'Save'
      case 'editing': return 'Cancel'
    }
  }
}
過渡模式

同時生效的進入和離開的過渡不能滿足所有要求,所以 Vue 提供了 過渡模式

  • in-out:新元素先進行過渡,完成之後當前元素過渡離開。
  • out-in:當前元素先進行過渡,完成之後新元素過渡進入。

out-in 重寫之前的開關按鈕過渡:

<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>
多個組件的過渡

多個組件的過渡簡單很多 - 我們不需要使用 key 特性。相反,我們只需要使用動態組件:

<transition name="component-fade" mode="out-in">
  <component v-bind:is="view"></component>
</transition>
new Vue({
  el: '#transition-components-demo',
  data: {
    view: 'v-a'
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>'
    },
    'v-b': {
      template: '<div>Component B</div>'
    }
  }
})
.component-fade-enter-active, .component-fade-leave-active {
  transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active for below version 2.1.8 */ {
  opacity: 0;
}
列表過渡

目前爲止,關於過渡我們已經講到:

  • 單個節點
  • 同一時間渲染多個節點中的一個

那麼怎麼同時渲染整個列表,比如使用 v-for ?在這種場景中,使用 <transition-group> 組件。在我們深入例子之前,先了解關於這個組件的幾個特點:

  • 不同於 <transition>,它會以一個真實元素呈現:默認爲一個 <span>。你也可以通過 tag 特性更換爲其他元素。
  • 內部元素 總是需要 提供唯一的 key 屬性值
列表的進入/離開過渡

現在讓我們由一個簡單的例子深入,進入和離開的過渡使用之前一樣的 CSS 類名。

<div id="list-demo" class="demo">
  <button v-on:click="add">Add</button>
  <button v-on:click="remove">Remove</button>
  <transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
  </transition-group>
</div>
new Vue({
  el: '#list-demo',
  data: {
    items: [1,2,3,4,5,6,7,8,9],
    nextNum: 10
  },
  methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove: function () {
      this.items.splice(this.randomIndex(), 1)
    },
  }
})
.list-item {
  display: inline-block;
  margin-right: 10px;
}
.list-enter-active, .list-leave-active {
  transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
  opacity: 0;
  transform: translateY(30px);
}
列表的排序過渡

<transition-group> 組件還有一個特殊之處。不僅可以進入和離開動畫,還可以改變定位。要使用這個新功能只需瞭解新增的 v-move 特性,它會在元素的改變定位的過程中應用。像之前的類名一樣,可以通過 name 屬性來自定義前綴,也可以通過 move-class 屬性手動設置。

v-move 對於設置過渡的切換時機和過渡曲線非常有用,你會看到如下的例子:

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="flip-list-demo" class="demo">
  <button v-on:click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" v-bind:key="item">
      {{ item }}
    </li>
  </transition-group>
</div>
new Vue({
  el: '#flip-list-demo',
  data: {
    items: [1,2,3,4,5,6,7,8,9]
  },
  methods: {
    shuffle: function () {
      this.items = _.shuffle(this.items)
    }
  }
})
.flip-list-move {
  transition: transform 1s;
}

這個看起來很神奇,內部的實現,Vue 使用了一個叫 FLIP 簡單的動畫隊列
使用 transforms 將元素從之前的位置平滑過渡新的位置。

我們將之前實現的例子和這個技術結合,使我們列表的一切變動都會有動畫過渡。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>

<div id="list-complete-demo" class="demo">
  <button v-on:click="shuffle">Shuffle</button>
  <button v-on:click="add">Add</button>
  <button v-on:click="remove">Remove</button>
  <transition-group name="list-complete" tag="p">
    <span
      v-for="item in items"
      v-bind:key="item"
      class="list-complete-item"
    >
      {{ item }}
    </span>
  </transition-group>
</div>
new Vue({
  el: '#list-complete-demo',
  data: {
    items: [1,2,3,4,5,6,7,8,9],
    nextNum: 10
  },
  methods: {
    randomIndex: function () {
      return Math.floor(Math.random() * this.items.length)
    },
    add: function () {
      this.items.splice(this.randomIndex(), 0, this.nextNum++)
    },
    remove: function () {
      this.items.splice(this.randomIndex(), 1)
    },
    shuffle: function () {
      this.items = _.shuffle(this.items)
    }
  }
})
.list-complete-item {
  transition: all 1s;
  display: inline-block;
  margin-right: 10px;
}
.list-complete-enter, .list-complete-leave-to
/* .list-complete-leave-active for below version 2.1.8 */ {
  opacity: 0;
  transform: translateY(30px);
}
.list-complete-leave-active {
  position: absolute;
}

需要注意的是使用 FLIP 過渡的元素不能設置爲 display: inline 。作爲替代方案,可以設置爲 display: inline-block 或者放置於 flex 中

FLIP 動畫不僅可以實現單列過渡,多維網格也同樣可以過渡

列表的交錯過渡

通過 data 屬性與 JavaScript 通信 ,就可以實現列表的交錯過渡:

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="staggered-list-demo">
  <input v-model="query">
  <transition-group
    name="staggered-fade"
    tag="ul"
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
  >
    <li
      v-for="(item, index) in computedList"
      v-bind:key="item.msg"
      v-bind:data-index="index"
    >{{ item.msg }}</li>
  </transition-group>
</div>
new Vue({
  el: '#staggered-list-demo',
  data: {
    query: '',
    list: [
      { msg: 'Bruce Lee' },
      { msg: 'Jackie Chan' },
      { msg: 'Chuck Norris' },
      { msg: 'Jet Li' },
      { msg: 'Kung Fury' }
    ]
  },
  computed: {
    computedList: function () {
      var vm = this
      return this.list.filter(function (item) {
        return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
      })
    }
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
      el.style.height = 0
    },
    enter: function (el, done) {
      var delay = el.dataset.index * 150
      setTimeout(function () {
        Velocity(
          el,
          { opacity: 1, height: '1.6em' },
          { complete: done }
        )
      }, delay)
    },
    leave: function (el, done) {
      var delay = el.dataset.index * 150
      setTimeout(function () {
        Velocity(
          el,
          { opacity: 0, height: 0 },
          { complete: done }
        )
      }, delay)
    }
  }
})
可複用的過渡

過渡可以通過 Vue 的組件系統實現複用。要創建一個可複用過渡組件,你需要做的就是將 <transition> 或者 <transition-group> 作爲根組件,然後將任何子組件放置在其中就可以了。

使用 template 的簡單例子:

Vue.component('my-special-transition', {
  template: '\
    <transition\
      name="very-special-transition"\
      mode="out-in"\
      v-on:before-enter="beforeEnter"\
      v-on:after-enter="afterEnter"\
    >\
      <slot></slot>\
    </transition>\
  ',
  methods: {
    beforeEnter: function (el) {
      // ...
    },
    afterEnter: function (el) {
      // ...
    }
  }
})

函數組件更適合完成這個任務:

Vue.component('my-special-transition', {
  functional: true,
  render: function (createElement, context) {
    var data = {
      props: {
        name: 'very-special-transition',
        mode: 'out-in'
      },
      on: {
        beforeEnter: function (el) {
          // ...
        },
        afterEnter: function (el) {
          // ...
        }
      }
    }
    return createElement('transition', data, context.children)
  }
})
動態過渡

在 Vue 中即使是過渡也是數據驅動的!動態過渡最基本的例子是通過 name 特性來綁定動態值。

<transition v-bind:name="transitionName">
  <!-- ... -->
</transition>

當你想用 Vue 的過渡系統來定義的 CSS 過渡/動畫 在不同過渡間切換會非常有用。

所有的過渡特性都是動態綁定。它不僅是簡單的特性,通過事件的鉤子函數方法,可以在獲取到相應上下文數據。這意味着,可以根據組件的狀態通過 JavaScript 過渡設置不同的過渡效果。

<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>

<div id="dynamic-fade-demo" class="demo">
  Fade In: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
  Fade Out: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
  <transition
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
  >
    <p v-if="show">hello</p>
  </transition>
  <button
    v-if="stop"
    v-on:click="stop = false; show = false"
  >Start animating</button>
  <button
    v-else
    v-on:click="stop = true"
  >Stop it!</button>
</div>
new Vue({
  el: '#dynamic-fade-demo',
  data: {
    show: true,
    fadeInDuration: 1000,
    fadeOutDuration: 1000,
    maxFadeDuration: 1500,
    stop: true
  },
  mounted: function () {
    this.show = false
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
    },
    enter: function (el, done) {
      var vm = this
      Velocity(el,
        { opacity: 1 },
        {
          duration: this.fadeInDuration,
          complete: function () {
            done()
            if (!vm.stop) vm.show = false
          }
        }
      )
    },
    leave: function (el, done) {
      var vm = this
      Velocity(el,
        { opacity: 0 },
        {
          duration: this.fadeOutDuration,
          complete: function () {
            done()
            vm.show = true
          }
        }
      )
    }
  }
})

最後,創建動態過渡的最終方案是組件通過接受 props 來動態修改之前的過渡。一句老話,唯一的限制是你的想象力。

狀態過渡

Vue 的過渡系統提供了非常多簡單的方法設置進入、離開和列表的動效。那麼對於數據元素本身的動效呢,比如:

  • 數字和運算
  • 顏色的顯示
  • SVG 節點的位置
  • 元素的大小和其他的屬性

所有的原始數字都被事先存儲起來,可以直接轉換到數字。做到這一步,我們就可以結合 Vue 的響應式和組件系統,使用第三方庫來實現切換元素的過渡狀態。

狀態動畫與偵聽器

通過偵聽器我們能監聽到任何數值屬性的數值更新。可能聽起來很抽象,所以讓我們先來看看使用 Tweenjs 一個例子:

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<div id="animated-number-demo">
  <input v-model.number="number" type="number" step="20">
  <p>{{ animatedNumber }}</p>
</div>
new Vue({
  el: '#animated-number-demo',
  data: {
    number: 0,
    animatedNumber: 0
  },
  watch: {
    number: function(newValue, oldValue) {
      var vm = this
      function animate () {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }

      new TWEEN.Tween({ tweeningNumber: oldValue })
        .easing(TWEEN.Easing.Quadratic.Out)
        .to({ tweeningNumber: newValue }, 500)
        .onUpdate(function () {
          vm.animatedNumber = this.tweeningNumber.toFixed(0)
        })
        .start()

      animate()
    }
  }
})

當你把數值更新時,就會觸發動畫。這個是一個不錯的演示,但是對於不能直接像數字一樣存儲的值,比如 CSS 中的 color 的值,通過下面的例子我們來通過 Color.js 實現一個例子:

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<div id="example-7">
  <input
    v-model="colorQuery"
    v-on:keyup.enter="updateColor"
    placeholder="Enter a color"
  >
  <button v-on:click="updateColor">Update</button>
  <p>Preview:</p>
  <span
    v-bind:style="{ backgroundColor: tweenedCSSColor }"
    class="example-7-color-preview"
  ></span>
  <p>{{ tweenedCSSColor }}</p>
</div>
var Color = net.brehaut.Color

new Vue({
  el: '#example-7',
  data: {
    colorQuery: '',
    color: {
      red: 0,
      green: 0,
      blue: 0,
      alpha: 1
    },
    tweenedColor: {}
  },
  created: function () {
    this.tweenedColor = Object.assign({}, this.color)
  },
  watch: {
    color: function () {
      function animate () {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }

      new TWEEN.Tween(this.tweenedColor)
        .to(this.color, 750)
        .start()

      animate()
    }
  },
  computed: {
    tweenedCSSColor: function () {
      return new Color({
        red: this.tweenedColor.red,
        green: this.tweenedColor.green,
        blue: this.tweenedColor.blue,
        alpha: this.tweenedColor.alpha
      }).toCSS()
    }
  },
  methods: {
    updateColor: function () {
      this.color = new Color(this.colorQuery).toRGB()
      this.colorQuery = ''
    }
  }
})
.example-7-color-preview {
  display: inline-block;
  width: 50px;
  height: 50px;
}
動態狀態過渡

就像 Vue 的過渡組件一樣,數據背後狀態過渡會實時更新,這對於原型設計十分有用。當你修改一些變量,即使是一個簡單的 SVG 多邊形也可實現很多難以想象的效果。

demo

把過渡放到組件裏

管理太多的狀態過渡會很快的增加 Vue 實例或者組件的複雜性,幸好很多的動畫可以提取到專用的子組件。
我們來將之前的示例改寫一下:

<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>

<div id="example-8">
  <input v-model.number="firstNumber" type="number" step="20"> +
  <input v-model.number="secondNumber" type="number" step="20"> =
  {{ result }}
  <p>
    <animated-integer v-bind:value="firstNumber"></animated-integer> +
    <animated-integer v-bind:value="secondNumber"></animated-integer> =
    <animated-integer v-bind:value="result"></animated-integer>
  </p>
</div>
// 這種複雜的補間動畫邏輯可以被複用
// 任何整數都可以執行動畫
// 組件化使我們的界面十分清晰
// 可以支持更多更復雜的動態過渡
// 策略。
Vue.component('animated-integer', {
  template: '<span>{{ tweeningValue }}</span>',
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  data: function () {
    return {
      tweeningValue: 0
    }
  },
  watch: {
    value: function (newValue, oldValue) {
      this.tween(oldValue, newValue)
    }
  },
  mounted: function () {
    this.tween(0, this.value)
  },
  methods: {
    tween: function (startValue, endValue) {
      var vm = this
      function animate () {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }

      new TWEEN.Tween({ tweeningValue: startValue })
        .to({ tweeningValue: endValue }, 500)
        .onUpdate(function (object) {
          vm.tweeningValue = object.tweeningValue.toFixed(0)
        })
        .start()

      animate()
    }
  }
})

// 所有的複雜度都已經從 Vue 的主實例中移除!
new Vue({
  el: '#example-8',
  data: {
    firstNumber: 20,
    secondNumber: 40
  },
  computed: {
    result: function () {
      return this.firstNumber + this.secondNumber
    }
  }
})

我們能在組件中結合使用這一節講到各種過渡策略和 Vue 內建的過渡系統。總之,對於完成各種過渡動效幾乎沒有阻礙。

賦予設計以生命

只要一個動畫,就可以帶來生命。不幸的是,當設計師創建圖標、logo 和吉祥物的時候,他們交付的通常都是圖片或靜態的 SVG。所以,雖然 GitHub 的章魚貓、Twitter 的小鳥以及其它許多 logo 類似於生靈,它們看上去實際上並不是活着的。

Vue 可以幫到你。因爲 SVG 的本質是數據,我們只需要這些動物興奮、思考或境界的樣例。然後 Vue 就可以輔助完成這幾種狀態之間的過渡動畫,來製作你的歡迎頁面、加載指示、以及更加帶有情感的提示。

Sarah Drasner 展示了下面這個 demo,這個 demo 結合了時間和交互相關的狀態改變:

可複用性 & 組合

混入

基礎

混入 (mixins) 是一種分發 Vue 組件中可複用功能的非常靈活的方式。混入對象可以包含任意組件選項。當組件使用混入對象時,所有混入對象的選項將被混入該組件本身的選項。

例子:

// 定義一個混入對象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定義一個使用混入對象的組件
var Component = Vue.extend({
  mixins: [myMixin]
})

var component = new Component() // => "hello from mixin!"
選項合併

當組件和混入對象含有同名選項時,這些選項將以恰當的方式混合。

比如,數據對象在內部會進行淺合併 (一層屬性深度),在和組件的數據發生衝突時以組件數據優先。

var mixin = {
  data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

new Vue({
  mixins: [mixin],
  data: function () {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created: function () {
    console.log(this.$data)
    // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

同名鉤子函數將混合爲一個數組,因此都將被調用。另外,混入對象的鉤子將在組件自身鉤子之前調用。

var mixin = {
  created: function () {
    console.log('混入對象的鉤子被調用')
  }
}

new Vue({
  mixins: [mixin],
  created: function () {
    console.log('組件鉤子被調用')
  }
})

// => "混入對象的鉤子被調用"
// => "組件鉤子被調用"

值爲對象的選項,例如 methods, componentsdirectives,將被混合爲同一個對象。兩個對象鍵名衝突時,取組件對象的鍵值對。

var mixin = {
  methods: {
    foo: function () {
      console.log('foo')
    },
    conflicting: function () {
      console.log('from mixin')
    }
  }
}

var vm = new Vue({
  mixins: [mixin],
  methods: {
    bar: function () {
      console.log('bar')
    },
    conflicting: function () {
      console.log('from self')
    }
  }
})

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

注意:Vue.extend() 也使用同樣的策略進行合併。

全局混入

也可以全局註冊混入對象。注意使用! 一旦使用全局混入對象,將會影響到 所有 之後創建的 Vue 實例。使用恰當時,可以爲自定義對象注入處理邏輯。

// 爲自定義的選項 'myOption' 注入一個處理器。
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello!'
})
// => "hello!"

謹慎使用全局混入對象,因爲會影響到每個單獨創建的 Vue 實例 (包括第三方模板)。大多數情況下,只應當應用於自定義選項,就像上面示例一樣。也可以將其用作 Plugins 以避免產生重複應用

自定義選項合併策略

自定義選項將使用默認策略,即簡單地覆蓋已有值。如果想讓自定義選項以自定義邏輯合併,可以向 Vue.config.optionMergeStrategies 添加一個函數:

Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // return mergedVal
}

對於大多數對象選項,可以使用 methods 的合併策略:

var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods

更多高級的例子可以在 Vuex 的 1.x 混入策略裏找到:

const merge = Vue.config.optionMergeStrategies.computed
Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) {
  if (!toVal) return fromVal
  if (!fromVal) return toVal
  return {
    getters: merge(toVal.getters, fromVal.getters),
    state: merge(toVal.state, fromVal.state),
    actions: merge(toVal.actions, fromVal.actions)
  }
}

自定義指令

簡介

除了核心功能默認內置的指令 (v-model 和 v-show),Vue 也允許註冊自定義指令。注意,在 Vue2.0 中,代碼複用和抽象的主要形式是組件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。舉個聚焦輸入框的例子,如下:

當頁面加載時,該元素將獲得焦點 (注意:autofocus 在移動版 Safari 上不工作)。事實上,只要你在打開這個頁面後還沒點擊過任何內容,這個輸入框就應當還是處於聚焦狀態。現在讓我們用指令來實現這個功能:

// 註冊一個全局自定義指令 `v-focus`
Vue.directive('focus', {
  // 當被綁定的元素插入到 DOM 中時……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

如果想註冊局部指令,組件中也接受一個 directives 的選項:

directives: {
  focus: {
    // 指令的定義
    inserted: function (el) {
      el.focus()
    }
  }
}

然後你可以在模板中任何元素上使用新的 v-focus 屬性,如下:

<input v-focus>
鉤子函數

一個指令定義對象可以提供如下幾個鉤子函數 (均爲可選):

  • bind:只調用一次,指令第一次綁定到元素時調用。在這裏可以進行一次性的初始化設置。
  • inserted:被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。
  • update:所在組件的 VNode 更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新 (詳細的鉤子函數參數見下)。
  • componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新後調用。
  • unbind:只調用一次,指令與元素解綁時調用。

接下來我們來看一下鉤子函數的參數 (即 elbindingvnodeoldVnode)。

鉤子函數參數

指令鉤子函數會被傳入以下參數:

  • el:指令所綁定的元素,可以用來直接操作 DOM 。
  • binding:一個對象,包含以下屬性:
    • name:指令名,不包括 v- 前綴。
    • value:指令的綁定值,例如:v-my-directive="1 + 1" 中,綁定值爲 2
    • oldValue:指令綁定的前一個值,僅在 updatecomponentUpdated 鉤子中可用。無論值是否改變都可用。
    • expression:字符串形式的指令表達式。例如 v-my-directive="1 + 1" 中,表達式爲 "1 + 1"
    • arg:傳給指令的參數,可選。例如 v-my-directive:foo 中,參數爲 "foo"
    • modifiers:一個包含修飾符的對象。例如:v-my-directive.foo.bar 中,修飾符對象爲 { foo: true, bar: true }
  • vnode:Vue 編譯生成的虛擬節點。移步 VNode API 來了解更多詳情。
  • oldVnode:上一個虛擬節點,僅在 updatecomponentUpdated 鉤子中可用。

除了 el 之外,其它參數都應該是隻讀的,切勿進行修改。如果需要在鉤子之間共享數據,建議通過元素的 dataset 來進行。

這是一個使用了這些屬性的自定義鉤子樣例:

<div id="hook-arguments-example" v-demo:foo.a.b="message"></div>
Vue.directive('demo', {
  bind: function (el, binding, vnode) {
    var s = JSON.stringify
    el.innerHTML =
      'name: '       + s(binding.name) + '<br>' +
      'value: '      + s(binding.value) + '<br>' +
      'expression: ' + s(binding.expression) + '<br>' +
      'argument: '   + s(binding.arg) + '<br>' +
      'modifiers: '  + s(binding.modifiers) + '<br>' +
      'vnode keys: ' + Object.keys(vnode).join(', ')
  }
})

new Vue({
  el: '#hook-arguments-example',
  data: {
    message: 'hello!'
  }
})
函數簡寫

在很多時候,你可能想在 bindupdate 時觸發相同行爲,而不關心其它的鉤子。比如這樣寫:

Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})
對象字面量

如果指令需要多個值,可以傳入一個 JavaScript 對象字面量。記住,指令函數能夠接受所有合法的 JavaScript 表達式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
Vue.directive('demo', function (el, binding) {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text)  // => "hello!"
})

渲染函數 & JSX

基礎

Vue 推薦在絕大多數情況下使用 template 來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力,這就是 render 函數,它比 template 更接近編譯器。

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

在 HTML 層,我們決定這樣定義組件接口:

<anchored-heading :level="1">Hello world!</anchored-heading>

當我們開始寫一個通過 level prop 動態生成 heading 標籤的組件,你可能很快想到這樣實現:

<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

在這種場景中使用 template 並不是最好的選擇:首先代碼冗長,爲了在不同級別的標題中插入錨點元素,我們需要重複地使用 <slot></slot>

雖然模板在大多數組件中都非常好用,但是在這裏它就不是很簡潔的了。那麼,我們來嘗試使用 render 函數重寫上面的例子:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name 標籤名稱
      this.$slots.default // 子組件中的陣列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

簡單清晰很多!簡單來說,這樣代碼精簡很多,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道當你不使用 slot 屬性向組件中傳遞內容時,比如 anchored-heading 中的 Hello world!,這些子元素被存儲在組件實例中的 $slots.default中。如果你還不瞭解, 在深入 render 函數之前推薦閱讀 實例屬性 API

節點、樹以及虛擬 DOM

在深入渲染函數之前,瞭解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 爲例:

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

當瀏覽器讀到這些代碼時,它會建立一個“DOM 節點”樹來保持追蹤,如同你會畫一張家譜樹來追蹤家庭成員的發展一樣。

HTML 的 DOM 節點樹如下圖所示:

每個元素都是一個節點。每片文字也是一個節點。甚至註釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。

高效的更新所有這些節點會是比較困難的,不過所幸你不必再手動完成這個工作了。你只需要告訴 Vue 你希望頁面上的 HTML 是什麼,這可以是在一個模板裏:

<h1>{{ blogTitle }}</h1>

或者一個渲染函數裏:

render: function (createElement) {
  return createElement('h1', this.blogTitle)
}

在這兩種情況下,Vue 都會自動保持頁面的更新,即便 blogTitle 發生了改變。

虛擬 DOM

Vue 通過建立一個虛擬 DOM 對真實 DOM 發生的變化保持追蹤。請近距離看一下這行代碼:

return createElement('h1', this.blogTitle)

createElement 到底會返回什麼呢?其實不是一個實際的 DOM 元素。它更準確的名字可能是 createNodeDescription,因爲它所包含的信息會告訴 Vue 頁面上需要渲染什麼樣的節點,及其子節點。我們把這樣的節點描述爲“虛擬節點 (Virtual Node)”,也常簡寫它爲“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。

createElement 參數

接下來你需要熟悉的是如何在 createElement 函數中生成模板。這裏是 createElement 接受的參數:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個 HTML 標籤字符串,組件選項對象,或者一個返回值
  // 類型爲 String/Object 的函數,必要參數
  'div',

  // {Object}
  // 一個包含模板相關屬性的數據對象
  // 這樣,您可以在 template 中使用這些屬性。可選參數。
  {
    // (詳情見下一節)
  },

  // {String | Array}
  // 子節點 (VNodes),由 `createElement()` 構建而成,
  // 或使用字符串來生成“文本節點”。可選參數。
  [
    '先寫一些文字',
    createElement('h1', '一則頭條'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
深入 data 對象

有一件事要注意:正如在模板語法中,v-bind:classv-bind:style ,會被特別對待一樣,在 VNode 數據對象中,下列屬性名是級別最高的字段。該對象也允許你綁定普通的 HTML 特性,就像 DOM 屬性一樣,比如 innerHTML (這會取代 v-html 指令)。

{
  // 和`v-bind:class`一樣的 API
  'class': {
    foo: true,
    bar: false
  },
  // 和`v-bind:style`一樣的 API
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 正常的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 組件 props
  props: {
    myProp: 'bar'
  },
  // DOM 屬性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件監聽器基於 `on`
  // 所以不再支持如 `v-on:keyup.enter` 修飾器
  // 需要手動匹配 keyCode。
  on: {
    click: this.clickHandler
  },
  // 僅對於組件,用於監聽原生事件,而不是組件內部使用
  // `vm.$emit` 觸發的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
  // 賦值,因爲 Vue 已經自動爲你進行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // Scoped slots in the form of
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果組件是其他組件的子組件,需爲插槽指定名稱
  slot: 'name-of-slot',
  // 其他特殊頂層屬性
  key: 'myKey',
  ref: 'myRef'
}
完整示例

有了這些知識,我們現在可以完成我們最開始想實現的組件:

var getChildrenTextContent = function (children) {
  return children.map(function (node) {
    return node.children
      ? getChildrenTextContent(node.children)
      : node.text
  }).join('')
}

Vue.component('anchored-heading', {
  render: function (createElement) {
    // create kebabCase id
    var headingId = getChildrenTextContent(this.$slots.default)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^\-|\-$)/g, '')

    return createElement(
      'h' + this.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, this.$slots.default)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
約束

VNodes 必須唯一

組件樹中的所有 VNodes 必須是唯一的。這意味着,下面的 render function 是無效的:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // 錯誤-重複的 VNodes
    myParagraphVNode, myParagraphVNode
  ])
}

如果你真的需要重複很多次的元素/組件,你可以使用工廠函數來實現。例如,下面這個例子 render 函數完美有效地渲染了 20 個重複的段落:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}
使用 JavaScript 代替模板功能
v-ifv-for

由於使用原生的 JavaScript 來實現某些東西很簡單,Vue 的 render 函數沒有提供專用的 API。比如,template 中的 v-ifv-for

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

這些都會在 render 函數中被 JavaScript 的 if/elsemap 重寫:

render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}
v-model

render 函數中沒有與 v-model 相應的 api - 你必須自己來實現相應的邏輯:

render: function (createElement) {
  var self = this
  return createElement('input', {
    domProps: {
      value: self.value
    },
    on: {
      input: function (event) {
        self.value = event.target.value
        self.$emit('input', event.target.value)
      }
    }
  })
}

這就是深入底層要付出的,儘管麻煩了一些,但相對於 v-model 來說,你可以更靈活地控制。

事件 & 按鍵修飾符

對於 .passive.capture.once事件修飾符, Vue 提供了相應的前綴可以用於 on

Modifier(s) Prefix
.passive &
.capture !
.once ~
.capture.once or
.once.capture ~!

例如:

on: {
  '!click': this.doThisInCapturingMode,
  '~keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}

對於其他的修飾符,前綴不是很重要,因爲你可以在事件處理函數中使用事件方法:

Modifier(s) Equivalent in Handler
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
Keys:.enter, .13 if (event.keyCode !== 13) return (change 13 to another key code for other key modifiers)
Modifiers Keys:.ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (change ctrlKey to altKey, shiftKey, or metaKey, respectively)

這裏是一個使用所有修飾符的例子:

on: {
  keyup: function (event) {
    // 如果觸發事件的元素不是事件綁定的元素
    // 則返回
    if (event.target !== event.currentTarget) return
    // 如果按下去的不是 enter 鍵或者
    // 沒有同時按下 shift 鍵
    // 則返回
    if (!event.shiftKey || event.keyCode !== 13) return
    // 阻止 事件冒泡
    event.stopPropagation()
    // 阻止該元素默認的 keyup 事件
    event.preventDefault()
    // ...
  }
}
插槽

你可以從 this.$slots 獲取 VNodes 列表中的靜態內容:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}

還可以從 this.$scopedSlots 中獲得能用作函數的作用域插槽,這個函數返回 VNodes:

render: function (createElement) {
  // `<div><slot :text="msg"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.msg
    })
  ])
}

如果要用渲染函數向子組件中傳遞作用域插槽,可以利用 VNode 數據中的 scopedSlots 域:

render (createElement) {
  return createElement('div', [
    createElement('child', {
      // pass `scopedSlots` in the data object
      // in the form of { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}
JSX

如果你寫了很多 render 函數,可能會覺得痛苦:

createElement(
  'anchored-heading', {
    props: {
      level: 1
    }
  }, [
    createElement('span', 'Hello'),
    ' world!'
  ]
)

特別是模板如此簡單的情況下:

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

這就是爲什麼會有一個 Babel 插件,用於在 Vue 中使用 JSX 語法的原因,它可以讓我們回到更接近於模板的語法上。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

h 作爲 createElement 的別名是 Vue 生態系統中的一個通用慣例,實際上也是 JSX 所要求的,如果在作用域中 h 失去作用,在應用中會觸發報錯。

更多關於 JSX 映射到 JavaScript,閱讀 使用文檔

函數式組件

之前創建的錨點標題組件是比較簡單,沒有管理或者監聽任何傳遞給他的狀態,也沒有生命週期方法。它只是一個接收參數的函數。

在這個例子中,我們標記組件爲 functional,這意味它是無狀態 (沒有 data),無實例 (沒有 this 上下文)。

一個 函數式組件 就像這樣:

Vue.component('my-component', {
  functional: true,
  // 爲了彌補缺少的實例
  // 提供第二個參數作爲上下文
  render: function (createElement, context) {
    // ...
  },
  // Props 可選
  props: {
    // ...
  }
})

注意:在 2.3.0 之前的版本中,如果一個函數式組件想要接受 props,則 props 選項是必須的。在 2.3.0 或以上的版本中,你可以省略 props 選項,所有組件上的屬性都會被自動解析爲 props。

在 2.5.0 及以上版本中,如果你使用了單文件組件,那麼基於模板的函數式組件可以這樣聲明:

<template functional>
</template>

組件需要的一切都是通過上下文傳遞,包括:

  • props:提供 props 的對象
  • children: VNode 子節點的數組
  • slots: slots 對象
  • data:傳遞給組件的 data 對象
  • parent:對父組件的引用
  • listeners: (2.3.0+) 一個包含了組件上所註冊的 v-on 偵聽器的對象。這只是一個指向 data.on 的別名。
  • injections: (2.3.0+) 如果使用了 inject 選項,則該對象包含了應當被注入的屬性。

在添加 functional: true 之後,錨點標題組件的 render 函數之間簡單更新增加 context 參數,this.$slots.default 更新爲 context.children,之後this.level 更新爲 context.props.level

因爲函數式組件只是一個函數,所以渲染開銷也低很多。然而,對持久化實例的缺乏也意味着函數式組件不會出現在 Vue devtools 的組件樹裏。

在作爲包裝組件時它們也同樣非常有用,比如,當你需要做這些時:

  • 程序化地在多個組件中選擇一個
  • 在將 children, props, data 傳遞給子組件之前操作它們。

下面是一個依賴傳入 props 的值的 smart-list 組件例子,它能代表更多具體的組件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }

Vue.component('smart-list', {
  functional: true,
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items

      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList

      return UnorderedList
    }

    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  },
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  }
})
向子元素或子組件傳遞特性和事件

在普通組件中,沒有被定義爲 prop 的特性會自動添加到組件的根元素上,將現有的同名特性替換或與其智能合併。

然而函數式組件要求你顯示定義該行爲:

Vue.component('my-functional-button', {
  functional: true,
  render: function (createElement, context) {
    // 完全透明的傳入任何特性、事件監聽器、子結點等。
    return createElement('button', context.data, context.children)
  }
})

createElement 通過傳入 context.data 作爲第二個參數,我們就把 my-functional-button 上面所有的特性和事件監聽器都傳遞下去了。事實上這是非常透明的,那些事件甚至並不要求 .native 修飾符。

slots()children 對比

你可能想知道爲什麼同時需要 slots()childrenslots().default 不是和 children 類似的嗎?在一些場景中,是這樣,但是如果是函數式組件和下面這樣的 children 呢?

<my-functional-component>
  <p slot="foo">
    first
  </p>
  <p>second</p>
</my-functional-component>

對於這個組件,children 會給你兩個段落標籤,而 slots().default 只會傳遞第二個匿名段落標籤,slots().foo 會傳遞第一個具名段落標籤。同時擁有 childrenslots() ,因此你可以選擇讓組件通過 slot() 系統分發或者簡單的通過 children 接收,讓其他組件去處理。

模板編譯

你可能有興趣知道,Vue 的模板實際是編譯成了 render 函數。這是一個實現細節,通常不需要關心,但如果你想看看模板的功能是怎樣被編譯的,你會發現會非常有趣。下面是一個使用 Vue.compile 來實時編譯模板字符串的簡單 demo:

<div>
  <header>
    <h1>I'm a template!</h1>
  </header>
  <p v-if="message">
    {{ message }}
  </p>
  <p v-else>
    No message.
  </p>
</div>    

render:

function anonymous() {
  with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}

staticRenderFns:

_m(0): function anonymous() {
  with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}

插件

開發插件

插件通常會爲 Vue 添加全局功能。插件的範圍沒有限制——一般有下面幾種:

  1. 添加全局方法或者屬性,如: vue-custom-element
  2. 添加全局資源:指令/過濾器/過渡等,如 vue-touch
  3. 通過全局 mixin 方法添加一些組件選項,如: vue-router
  4. 添加 Vue 實例方法,通過把它們添加到 Vue.prototype 上實現。
  5. 一個庫,提供自己的 API,同時提供上面提到的一個或多個功能,如 vue-router

Vue.js 的插件應當有一個公開方法 install 。這個方法的第一個參數是 Vue 構造器,第二個參數是一個可選的選項對象:

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或屬性
  Vue.myGlobalMethod = function () {
    // 邏輯...
  }

  // 2. 添加全局資源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 邏輯...
    }
    ...
  })

  // 3. 注入組件
  Vue.mixin({
    created: function () {
      // 邏輯...
    }
    ...
  })

  // 4. 添加實例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 邏輯...
  }
}
使用插件

通過全局方法 Vue.use() 使用插件:

// 調用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

也可以傳入一個選項對象:

Vue.use(MyPlugin, { someOption: true })

Vue.use 會自動阻止多次註冊相同插件,屆時只會註冊一次該插件。

Vue.js 官方提供的一些插件 (例如 vue-router) 在檢測到 Vue 是可訪問的全局變量時會自動調用 Vue.use()。然而在例如 CommonJS 的模塊環境中,你應該始終顯式地調用 Vue.use()

// 用 Browserify 或 webpack 提供的 CommonJS 模塊環境時
var Vue = require('vue')
var VueRouter = require('vue-router')

// 不要忘了調用此方法
Vue.use(VueRouter)

awesome-vue 集合了來自社區貢獻的數以千計的插件和庫。

過濾器

Vue.js 允許你自定義過濾器,可被用於一些常見的文本格式化。過濾器可以用在兩個地方:雙花括號插值和 v-bind 表達式 (後者從 2.1.0+ 開始支持)。過濾器應該被添加在 JavaScript 表達式的尾部,由“管道”符號指示:

<!-- 在雙花括號中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

你可以在一個組件的選項中定義本地的過濾器:

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

或者在創建 Vue 實例之前全局定義過濾器:

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

過濾器函數總接收表達式的值 (之前的操作鏈的結果) 作爲第一個參數。在上述例子中,capitalize 過濾器函數將會收到 message 的值作爲第一個參數。

過濾器可以串聯:

{{ message | filterA | filterB }}

在這個例子中,filterA 被定義爲接收單個參數的過濾器函數,表達式 message 的值將作爲參數傳入到函數中。然後繼續調用同樣被定義爲接收單個參數的過濾器函數 filterB,將 filterA 的結果傳遞到 filterB 中。

過濾器是 JavaScript 函數,因此可以接收參數:

{{ message | filterA('arg1', arg2) }}

這裏,filterA 被定義爲接收三個參數的過濾器函數。其中 message 的值作爲第一個參數,普通字符串 'arg1' 作爲第二個參數,表達式 arg2 的值作爲第三個參數。

工具

生產環境部署

開啓生產環境模式

開發環境下,Vue 會提供很多警告來幫你對付常見的錯誤與陷阱。而在生產環境下,這些警告語句卻沒有用,反而會增加應用的體積。此外,有些警告檢查還有一些小的運行時開銷,這在生產環境模式下是可以避免的。

不使用構建工具

如果用 Vue 完整獨立版本,即直接用 <script> 元素引入 Vue 而不提前進行構建,請記得在生產環境下使用壓縮後的版本 (vue.min.js)。兩種版本都可以在安裝指導中找到。

使用構建工具

當使用 webpack 或 Browserify 類似的構建工具時,Vue 源碼會根據 process.env.NODE_ENV 決定是否啓用生產環境模式,默認情況爲開發環境模式。在 webpackBrowserify 中都有方法來覆蓋此變量,以啓用 Vue 的生產環境模式,同時在構建過程中警告語句也會被壓縮工具去除。這些所有 vue-cli 模板中都預先配置好了,但瞭解一下怎樣配置會更好。

webpack

使用 webpack 的 DefinePlugin 來指定生產環境,以便在壓縮時可以讓 UglifyJS 自動刪除警告代碼塊。例如配置:

var webpack = require('webpack')

module.exports = {
  // ...
  plugins: [
    // ...
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    })
  ]
}

Browserify

  • 在運行打包命令時將 NODE_ENV 設置爲 "production"。這等於告訴 vueify 避免引入熱重載和開發相關的代碼。
  • 對打包後的文件進行一次全局的 envify 轉換。這使得壓縮工具能清除調 Vue 源碼中所有用環境變量條件包裹起來的警告語句。例如:
NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js
  • 或者在 Gulp 中使用 envify
// 使用 envify 的自定義模塊來定製環境變量
var envify = require('envify/custom')

browserify(browserifyOptions)
  .transform(vueify)
  .transform(
    // 必填項,以處理 node_modules 裏的文件
    { global: true },
    envify({ NODE_ENV: 'production' })
  )
  .bundle()
// 使用 envify 自定義模塊指定環境變量
var envify = require('envify/custom')

browserify: {
  dist: {
    options: {
        // 該函數用來調整 grunt-browserify 的默認指令
        configure: b => b
        .transform('vueify')
        .transform(
            // 用來處理 `node_modules` 文件
          { global: true },
          envify({ NODE_ENV: 'production' })
        )
        .bundle()
    }
  }
}

Rollup

使用 rollup-plugin-replace

const replace = require('rollup-plugin-replace')
rollup({
  // ...
  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify( 'production' )
    })
  ]
}).then(...)
模板預編譯

當使用 DOM 內模板或 JavaScript 內的字符串模板時,模板會在運行時被編譯爲渲染函數。通常情況下這個過程已經足夠快了,但對性能敏感的應用還是最好避免這種用法。

預編譯模板最簡單的方式就是使用單文件組件——相關的構建設置會自動把預編譯處理好,所以構建好的代碼已經包含了編譯出來的渲染函數而不是原始的模板字符串。

如果你使用 webpack,並且喜歡分離 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在構建過程中把模板文件轉換成爲 JavaScript 渲染函數。

提取組件的 CSS

當使用單文件組件時,組件內的 CSS 會以 <style> 標籤的方式通過 JavaScript 動態注入。這有一些小小的運行時開銷,如果你使用服務端渲染,這會導致一段“無樣式內容閃爍 (fouc)”。將所有組件的 CSS 提取到同一個文件可以避免這個問題,也會讓 CSS 更好地進行壓縮和緩存。

查閱這個構建工具各自的文檔來了解更多:

跟蹤運行時錯誤

如果在組件渲染時出現運行錯誤,錯誤將會被傳遞至全局 Vue.config.errorHandler 配置函數 (如果已設置)。利用這個鉤子函數來配合錯誤跟蹤服務是個不錯的主意。比如 Sentry,它爲 Vue 提供了官方集成

單文件組件

介紹

在很多 Vue 項目中,我們使用 Vue.component 來定義全局組件,緊接着用 new Vue({ el: '#container '}) 在每個頁面內指定一個容器元素。

這種方式在很多中小規模的項目中運作的很好,在這些項目裏 JavaScript 只被用來加強特定的視圖。但當在更復雜的項目中,或者你的前端完全由 JavaScript 驅動的時候,下面這些缺點將變得非常明顯:

  • 全局定義 (Global definitions) 強制要求每個 component 中的命名不得重複
  • 字符串模板 (String templates) 缺乏語法高亮,在 HTML 有多行的時候,需要用到醜陋的 \
  • 不支持 CSS (No CSS support) 意味着當 HTML 和 JavaScript 組件化時,CSS 明顯被遺漏
  • 沒有構建步驟 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用預處理器,如 Pug (formerly Jade) 和 Babel

文件擴展名爲 .vuesingle-file components(單文件組件) 爲以上所有問題提供瞭解決方法,並且還可以使用 webpack 或 Browserify 等構建工具。

這是一個文件名爲 Hello.vue 的簡單實例:

現在我們獲得:

正如我們說過的,我們可以使用預處理器來構建簡潔和功能更豐富的組件,比如 Pug,Babel (with ES2015 modules),和 Stylus。

這些特定的語言只是例子,你可以只是簡單地使用 Babel,TypeScript,SCSS,PostCSS - 或者其他任何能夠幫助你提高生產力的預處理器。如果搭配 vue-loader 使用 webpack,它也是把 CSS Modules 當作第一公民來對待的。

怎麼看待關注點分離?

一個重要的事情值得注意,關注點分離不等於文件類型分離。在現代 UI 開發中,我們已經發現相比於把代碼庫分離成三個大的層次並將其相互交織起來,把它們劃分爲鬆散耦合的組件再將其組合起來更合理一些。在一個組件裏,其模板、邏輯和樣式是內部耦合的,並且把他們搭配在一起實際上使得組件更加內聚且更可維護。

即便你不喜歡單文件組件,你仍然可以把 JavaScript、CSS 分離成獨立的文件然後做到熱重載和預編譯。

<!-- my-component.vue -->
<template>
  <div>This will be pre-compiled</div>
</template>
<script src="./my-component.js"></script>
<style src="./my-component.css"></style>
起步
例子沙箱

如果你希望深入瞭解並開始使用單文件組件,請來 CodeSandbox 看看這個簡單的 todo 應用

針對剛接觸 JavaScript 模塊開發系統的用戶

有了 .vue 組件,我們就進入了高級 JavaScript 應用領域。如果你沒有準備好的話,意味着還需要學會使用一些附加的工具:

  • Node Package Manager (NPM):閱讀 Getting Started guide 直到 10: Uninstalling global packages章節。
  • Modern JavaScript with ES2015/16:閱讀 Babel 的 Learn ES2015 guide。你不需要立刻記住每一個方法,但是你可以保留這個頁面以便後期參考。

在你花一天時間瞭解這些資源之後,我們建議你參考 webpack 模板。只要遵循指示,你就能很快地運行一個用到 .vue 組件,ES2015 和熱重載 (hot-reloading) 的 Vue 項目!

想學習更多 webpack 的知識,請移步它們的官方文檔以及 webpack learning academy。在 webpack 中,每個模塊被打包到 bundle 之前都由一個相應的“loader”來轉換,Vue 也提供 vue-loader 插件來執行 .vue 單文件組件 的轉換。

針對高級用戶

無論你更鐘情 webpack 或是 Browserify,我們爲簡單的和更復雜的項目都提供了一些文檔模板。我們建議瀏覽 github.com/vuejs-templates,找到你需要的部分,然後參考 README 中的說明,使用 vue-cli 工具生成新的項目。

模板中使用 webpack,一個模塊加載器加載多個模塊然後構建成最終應用。爲了進一步瞭解 webpack,可以看 官方介紹視頻。如果你有基礎,可以看 在 Egghead.io 上的 webpack 進階教程

單元測試

配置和工具

任何兼容基於模塊的構建系統都可以正常使用,但如果你需要一個具體的建議,可以使用 Karma 進行自動化測試。它有很多社區版的插件,包括對 WebpackBrowserify 的支持。更多詳細的安裝步驟,請參考各項目的安裝文檔,通過這些 Karma 配置的例子可以快速幫助你上手 (Webpack 配置,Browserify 配置)。

簡單的斷言

你不必爲了可測性在組件中做任何特殊的操作,導出原始設置就可以了:

<template>
  <span>{{ message }}</span>
</template>

<script>
  export default {
    data () {
      return {
        message: 'hello!'
      }
    },
    created () {
      this.message = 'bye!'
    }
  }
</script>

然後隨着 Vue 導入組件的選項,你可以使用許多常見的斷言:

// 導入 Vue.js 和組件,進行測試
import Vue from 'vue'
import MyComponent from 'path/to/MyComponent.vue'

// 這裏是一些 Jasmine 2.0 的測試,你也可以使用你喜歡的任何斷言庫或測試工具。

describe('MyComponent', () => {
  // 檢查原始組件選項
  it('has a created hook', () => {
    expect(typeof MyComponent.created).toBe('function')
  })

  // 評估原始組件選項中的函數的結果
  it('sets the correct default data', () => {
    expect(typeof MyComponent.data).toBe('function')
    const defaultData = MyComponent.data()
    expect(defaultData.message).toBe('hello!')
  })

  // 檢查 mount 中的組件實例
  it('correctly sets the message when created', () => {
    const vm = new Vue(MyComponent).$mount()
    expect(vm.message).toBe('bye!')
  })

  // 創建一個實例並檢查渲染輸出
  it('renders the correct message', () => {
    const Constructor = Vue.extend(MyComponent)
    const vm = new Constructor().$mount()
    expect(vm.$el.textContent).toBe('bye!')
  })
})
編寫可被測試的組件

很多組件的渲染輸出由它的 props 決定。事實上,如果一個組件的渲染輸出完全取決於它的 props,那麼它會讓測試變得簡單,就好像斷言不同參數的純函數的返回值。看下面這個例子:

<template>
  <p>{{ msg }}</p>
</template>

<script>
  export default {
    props: ['msg']
  }
</script>

你可以在不同的 props 中,通過 propsData 選項斷言它的渲染輸出:

import Vue from 'vue'
import MyComponent from './MyComponent.vue'

// 掛載元素並返回已渲染的文本的工具函數
function getRenderedText (Component, propsData) {
  const Constructor = Vue.extend(Component)
  const vm = new Constructor({ propsData: propsData }).$mount()
  return vm.$el.textContent
}

describe('MyComponent', () => {
  it('renders correctly with different props', () => {
    expect(getRenderedText(MyComponent, {
      msg: 'Hello'
    })).toBe('Hello')

    expect(getRenderedText(MyComponent, {
      msg: 'Bye'
    })).toBe('Bye')
  })
})
斷言異步更新

由於 Vue 進行 異步更新 DOM 的情況,一些依賴 DOM 更新結果的斷言必須在 Vue.nextTick 回調中進行:

我們計劃做一個通用的測試工具集,讓不同策略的渲染輸出 (例如忽略子組件的基本渲染) 和斷言變得更簡單。

TypeScript 支持

在 Vue 2.5.0 中,我們大大改進了類型聲明以更好地使用默認的基於對象的 API。同時此版本也引入了一些其它變化,需要開發者作出相應的升級。閱讀博客文章瞭解更多詳情。

發佈爲 NPM 包的官方聲明文件

靜態類型系統能幫助你有效防止許多潛在的運行時錯誤,而且隨着你的應用日漸豐滿會更加顯著。這就是爲什麼 Vue 不僅僅爲 Vue core 提供了針對 TypeScript官方類型聲明,還爲 Vue RouterVuex 也提供了相應的聲明文件。

我們還計劃在近期爲 vue-cli 提供一個選項,來初始化一個立即可投入開發的 Vue + TypeScript 項目腳手架。

推薦配置
// tsconfig.json
{
  "compilerOptions": {
    // 與 Vue 的瀏覽器支持保持一致
    "target": "es5",
    // 這可以對 `this` 上的數據屬性進行更嚴格的推斷
    "strict": true,
    // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake:
    "module": "es2015",
    "moduleResolution": "node"
  }
}

注意你需要引入 strict: true (或者至少 noImplicitThis: true,這是 strict 模式的一部分) 以利用組件方法中 this 的類型檢查,否則它會始終被看作 any 類型。

參閱 TypeScript 編譯器選項文檔 (英) 瞭解更多。

開發工具鏈

要使用 TypeScript 開發 Vue 應用程序,我們強烈建議您使用 Visual Studio Code,它爲 TypeScript 提供了極好的“開箱即用”支持。

如果你正在使用單文件組件 (SFC), 可以安裝提供 SFC 支持以及其他更多實用功能的 Vetur 插件

WebStorm 同樣爲 TypeScript 和 Vue.js 提供了“開箱即用”的支持。

基本用法

要讓 TypeScript 正確推斷 Vue 組件選項中的類型,您需要使用 Vue.componentVue.extend 定義組件:

import Vue from 'vue'
const Component = Vue.extend({
  // 類型推斷已啓用
})

const Component = {
  // 這裏不會有類型推斷,
  // 因爲TypeScript不能確認這是Vue組件的選項
}
基於類的 Vue 組件

如果您在聲明組件時更喜歡基於類的 API,則可以使用官方維護的 vue-class-component 裝飾器:

import Vue from 'vue'
import Component from 'vue-class-component'

// @Component 修飾符註明了此類爲一個 Vue 組件
@Component({
  // 所有的組件選項都可以放在這裏
  template: '<button @click="onClick">Click!</button>'
})
export default class MyComponent extends Vue {
  // 初始數據可以直接聲明爲實例的屬性
  message: string = 'Hello!'

  // 組件方法也可以直接聲明爲實例的方法
  onClick (): void {
    window.alert(this.message)
  }
}
增強類型以配合插件使用

插件可以增加 Vue 的全局/實例屬性和組件選項。在這些情況下,在 TypeScript 中製作插件需要類型聲明。慶幸的是,TypeScript 有一個特性來補充現有的類型,叫做模塊補充 (module augmentation)

例如,聲明一個 string 類型的實例屬性 $myProperty

// 1. 確保在聲明補充的類型之前導入 'vue'
import Vue from 'vue'

// 2. 定製一個文件,設置你想要補充的類型
//    在 types/vue.d.ts 裏 Vue 有構造函數類型
declare module 'vue/types/vue' {
// 3. 聲明爲 Vue 補充的東西
  interface Vue {
    $myProperty: string
  }
}

在你的項目中包含了上述作爲聲明文件的代碼之後 (像 my-property.d.ts),你就可以在 Vue 實例上使用 $myProperty 了。

var vm = new Vue()
console.log(vm.$myProperty) // 將會順利編譯通過

你也可以聲明額外的屬性和組件選項:

import Vue from 'vue'

declare module 'vue/types/vue' {
  // 可以使用 `VueConstructor` 接口
  // 來聲明全局屬性
  interface VueConstructor {
    $myGlobal: string
  }
}

// ComponentOptions 聲明於 types/options.d.ts 之中
declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    myOption?: string
  }
}

上述的聲明允許下面的代碼順利編譯通過:

// 全局屬性
console.log(Vue.$myGlobal)

// 額外的組件選項
var vm = new Vue({
  myOption: 'Hello'
})
標註返回值

因爲 Vue 的聲明文件天生就具有循環性,TypeScript 可能在推斷某個方法的類型的時候存在困難。因此,你可能需要在 rendercomputed 裏的方法上標註返回值。

import Vue, { VNode } from 'vue'

const Component = Vue.extend({
  data () {
    return {
      msg: 'Hello'
    }
  },
  methods: {
    // 需要標註有 `this` 參與運算的返回值類型
    greet (): string {
      return this.msg + ' world'
    }
  },
  computed: {
    // 需要標註
    greeting(): string {
      return this.greet() + '!'
    }
  },
  // `createElement` 是可推導的,但是 `render` 需要返回值類型
  render (createElement): VNode {
    return createElement('div', this.greeting)
  }
})

如果你發現類型推導或成員補齊不工作了,標註某個方法也許可以幫助你解決這個問題。使用 --noImplicitAny 選項將會幫助你找到這些未標註的方法。

規模化

路由

官方路由

對於大多數單頁面應用,都推薦使用官方支持的 vue-router 庫。更多細節可以看 vue-router 文檔。

從零開始簡單的路由

如果只需要非常簡單的路由而不需要引入整個路由庫,可以動態渲染一個頁面級的組件像這樣:

const NotFound = { template: '<p>Page not found</p>' }
const Home = { template: '<p>home page</p>' }
const About = { template: '<p>about page</p>' }

const routes = {
  '/': Home,
  '/about': About
}

new Vue({
  el: '#app',
  data: {
    currentRoute: window.location.pathname
  },
  computed: {
    ViewComponent () {
      return routes[this.currentRoute] || NotFound
    }
  },
  render (h) { return h(this.ViewComponent) }
})

結合 HTML5 History API,你可以建立一個非常基本但功能齊全的客戶端路由器。可以直接看實例應用

整合第三方路由

如果有非常喜歡的第三方路由,如 Page.js 或者 Director,整合很簡單。這有個用了 Page.js 的複雜示例

狀態管理

類 Flux 狀態管理的官方實現

由於多個狀態分散的跨越在許多組件和交互間各個角落,大型應用複雜度也經常逐漸增長。爲了解決這個問題,Vue 提供 vuex:我們有受到 Elm 啓發的狀態管理庫。vuex 甚至集成到 vue-devtools,無需配置即可訪問時光旅行。

React 的開發者請參考以下信息

如果你是來自 React 的開發者,你可能會對 Vuex 和 Redux 間的差異表示關注,Redux 是 React 生態環境中最流行的 Flux 實現。Redux 事實上無法感知視圖層,所以它能夠輕鬆的通過一些簡單綁定和 Vue 一起使用。Vuex 區別在於它是一個專門爲 Vue 應用所設計。這使得它能夠更好地和 Vue 進行整合,同時提供簡潔的 API 和改善過的開發體驗。

簡單狀態管理起步使用

經常被忽略的是,Vue 應用中原始數據對象的實際來源 - 當訪問數據對象時,一個 Vue 實例只是簡單的代理訪問。所以,如果你有一處需要被多個實例間共享的狀態,可以簡單地通過維護一份數據來實現共享:

const sourceOfTruth = {}

const vmA = new Vue({
  data: sourceOfTruth
})

const vmB = new Vue({
  data: sourceOfTruth
})

現在當 sourceOfTruth 發生變化,vmAvmB 都將自動的更新引用它們的視圖。子組件們的每個實例也會通過 this.$root.$data 去訪問。現在我們有了唯一的實際來源,但是,調試將會變爲噩夢。任何時間,我們應用中的任何部分,在任何數據改變後,都不會留下變更過的記錄。

爲了解決這個問題,我們採用一個簡單的 store 模式

var store = {
  debug: true,
  state: {
    message: 'Hello!'
  },
  setMessageAction (newValue) {
    if (this.debug) console.log('setMessageAction triggered with', newValue)
    this.state.message = newValue
  },
  clearMessageAction () {
    if (this.debug) console.log('clearMessageAction triggered')
    this.state.message = ''
  }
}

需要注意,所有 store 中 state 的改變,都放置在 store 自身的 action 中去管理。這種集中式狀態管理能夠被更容易地理解哪種類型的 mutation 將會發生,以及它們是如何被觸發。當錯誤出現時,我們現在也會有一個 log 記錄 bug 之前發生了什麼。

此外,每個實例/組件仍然可以擁有和管理自己的私有狀態:

var vmA = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

var vmB = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

重要的是,注意你不應該在 action 中 替換原始的狀態對象 - 組件和 store 需要引用同一個共享對象,mutation 才能夠被觀察

接着我們繼續延伸約定,組件不允許直接修改屬於 store 實例的 state,而應執行 action 來分發 (dispatch) 事件通知 store 去改變,我們最終達成了 Flux 架構。這樣約定的好處是,我們能夠記錄所有 store 中發生的 state 改變,同時實現能做到記錄變更 (mutation)、保存狀態快照、歷史回滾/時光旅行的先進的調試工具。

說了一圈其實又回到了vuex,如果你已經讀到這兒,或許可以去嘗試一下!

服務端渲染

SSR 完全指南

在 2.3 發佈後我們發佈了一份完整的構建 Vue 服務端渲染應用的指南。這份指南非常深入,適合已經熟悉 Vue, webpack 和 Node.js 開發的開發者閱讀。請移步 ssr.vuejs.org

Nuxt.js

從頭搭建一個服務端渲染的應用是相當複雜的。幸運的是,我們有一個優秀的社區項目 Nuxt.js 讓這一切變得非常簡單。Nuxt 是一個基於 Vue 生態的更高層的框架,爲開發服務端渲染的 Vue 應用提供了極其便利的開發體驗。更酷的是,你甚至可以用它來做爲靜態站生成器。推薦嘗試。

內在

深入響應式原理

現在是時候深入一下了!Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。這使得狀態管理非常簡單直接,不過理解其工作原理同樣重要,這樣你可以迴避一些常見的問題。在這個章節,我們將進入一些 Vue 響應式系統的底層的細節。

如何追蹤變化

當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象所有的屬性,並使用 Object.defineProperty 把這些屬性全部轉爲 getter/setter。Object.defineProperty 是 ES5 中一個無法 shim 的特性,這也就是爲什麼 Vue 不支持 IE8 以及更低版本瀏覽器的原因。

用戶看不到 getter/setter,但是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化。這裏需要注意的問題是瀏覽器控制檯在打印數據對象時 getter/setter 的格式化並不同,所以你可能需要安裝 vue-devtools 來獲取更加友好的檢查接口。

每個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄爲依賴,之後當依賴項的 setter 被調用時,會通知 watcher 重新計算,從而致使它關聯的組件得以更新。

檢測變化的注意事項

受現代 JavaScript 的限制 (以及廢棄 Object.observe),Vue 不能檢測到對象屬性的添加或刪除。由於 Vue 會在初始化實例時對屬性執行 getter/setter 轉化過程,所以屬性必須在 data 對象上存在才能讓 Vue 轉換它,這樣才能讓它是響應的。例如:

var vm = new Vue({
  data:{
  a:1
  }
})

// `vm.a` 是響應的

vm.b = 2
// `vm.b` 是非響應的

Vue 不允許在已經創建的實例上動態添加新的根級響應式屬性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法將響應屬性添加到嵌套的對象上:

Vue.set(vm.someObject, 'b', 2)

您還可以使用 vm.$set 實例方法,這也是全局 Vue.set 方法的別名:

this.$set(this.someObject,'b',2)

有時你想向已有對象上添加一些屬性,例如使用 Object.assign()_.extend() 方法來添加屬性。但是,添加到對象上的新屬性不會觸發更新。在這種情況下可以創建一個新的對象,讓它包含原對象的屬性和新的屬性:

// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

也有一些數組相關的問題,之前已經在列表渲染中講過。

聲明響應式屬性

由於 Vue 不允許動態添加根級響應式屬性,所以你必須在初始化實例前聲明根級響應式屬性,哪怕只是一個空值:

var vm = new Vue({
  data: {
    // 聲明 message 爲一個空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之後設置 `message`
vm.message = 'Hello!'

如果你在 data 選項中未聲明 message,Vue 將警告你渲染函數在試圖訪問的屬性不存在。

這樣的限制在背後是有其技術原因的,它消除了在依賴項跟蹤系統中的一類邊界情況,也使 Vue 實例在類型檢查系統的幫助下運行的更高效。而且在代碼可維護性方面也有一點重要的考慮:data 對象就像組件狀態的概要,提前聲明所有的響應式屬性,可以讓組件代碼在以後重新閱讀或其他開發人員閱讀時更易於被理解。

異步更新隊列

可能你還沒有注意到,Vue 異步執行 DOM 更新。只要觀察到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的所有數據改變。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免不必要的計算和 DOM 操作上非常重要。然後,在下一個的事件循環“tick”中,Vue 刷新隊列並執行實際 (已去重的) 工作。Vue 在內部嘗試對異步隊列使用原生的 Promise.thenMessageChannel,如果執行環境不支持,會採用 setTimeout(fn, 0) 代替。

例如,當你設置 vm.someData = 'new value' ,該組件不會立即重新渲染。當刷新隊列時,組件會在事件循環隊列清空時的下一個“tick”更新。多數情況我們不需要關心這個過程,但是如果你想在 DOM 狀態更新後做點什麼,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發人員沿着“數據驅動”的方式思考,避免直接接觸 DOM,但是有時我們確實要這麼做。爲了在數據變化之後等待 Vue 完成更新 DOM ,可以在數據變化之後立即使用 Vue.nextTick(callback) 。這樣回調函數在 DOM 更新完成後就會調用。例如:

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改數據
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在組件內使用 vm.$nextTick() 實例方法特別方便,因爲它不需要全局 Vue ,並且回調函數中的 this 將自動綁定到當前的 Vue 實例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '沒有更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '更新完成'
      console.log(this.$el.textContent) // => '沒有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})

更多

對比其他框架

這個頁面無疑是最難編寫的,但我們認爲它也是非常重要的。或許你曾遇到了一些問題並且已經用其他的框架解決了。你來這裏的目的是看看 Vue 是否有更好的解決方案。這也是我們在此想要回答的。

客觀來說,作爲核心團隊成員,顯然我們會更偏愛 Vue,認爲對於某些問題來講用 Vue 解決會更好。如果沒有這點信念,我們也就不會整天爲此忙活了。但是在此,我們想盡可能地公平和準確地來描述一切。其他的框架也有顯著的優點,例如 React 龐大的生態系統,或者像是 Knockout 對瀏覽器的支持覆蓋到了 IE6。我們會嘗試着把這些內容全部列出來。

我們也希望得到你的幫助,來使文檔保持最新狀態,因爲 JavaScript 的世界進步的太快。如果你注意到一個不準確或似乎不太正確的地方,請提交問題讓我們知道。

React

React 和 Vue 有許多相似之處,它們都有:

  • 使用 Virtual DOM
  • 提供了響應式 (Reactive) 和組件化 (Composable) 的視圖組件。
  • 將注意力集中保持在覈心庫,而將其他功能如路由和全局狀態管理交給相關的庫。

由於有着衆多的相似處,我們會用更多的時間在這一塊進行比較。這裏我們不只保證技術內容的準確性,同時也兼顧了平衡的考量。我們需要承認 React 比 Vue 更好的地方,比如更豐富的生態系統。

React 社區爲我們準確進行平衡的考量提供了非常積極的幫助,特別感謝來自 React 團隊的 Dan Abramov 。他非常慷慨的花費時間來貢獻專業知識來幫助我們完善這篇文檔。

運行時性能

React 和 Vue 都是非常快的,所以速度並不是在它們之中做選擇的決定性因素。對於具體的數據表現,可以移步這個第三方 benchmark,它專注於渲染/更新非常簡單的組件樹的真實性能。

優化

在 React 應用中,當某個組件的狀態發生變化時,它會以該組件爲根,重新渲染整個組件子樹。

如要避免不必要的子組件的重渲染,你需要在所有可能的地方使用 PureComponent,或是手動實現 shouldComponentUpdate 方法。同時你可能會需要使用不可變的數據結構來使得你的組件更容易被優化。

然而,使用 PureComponentshouldComponentUpdate 時,需要保證該組件的整個子樹的渲染輸出都是由該組件的 props 所決定的。如果不符合這個情況,那麼此類優化就會導致難以察覺的渲染結果不一致。這使得 React 中的組件優化伴隨着相當的心智負擔。

在 Vue 應用中,組件的依賴是在渲染過程中自動追蹤的,所以系統能精確知曉哪個組件確實需要被重渲染。你可以理解爲每一個組件都已經自動獲得了 shouldComponentUpdate,並且沒有上述的子樹問題限制。

Vue 的這個特點使得開發者不再需要考慮此類優化,從而能夠更好地專注於應用本身。

HTML & CSS

在 React 中,一切都是 JavaScript。不僅僅是 HTML 可以用 JSX 來表達,現在的潮流也越來越多地將 CSS 也納入到 JavaScript 中來處理。這類方案有其優點,但也存在一些不是每個開發者都能接受的取捨。

Vue 的整體思想是擁抱經典的 Web 技術,並在其上進行擴展。我們下面會詳細分析一下。

JSX vs Templates

在 React 中,所有的組件的渲染功能都依靠 JSX。JSX 是使用 XML 語法編寫 JavaScript 的一種語法糖。

JSX 說是手寫的渲染函數有下面這些優勢:

  • 你可以使用完整的編程語言 JavaScript 功能來構建你的視圖頁面。比如你可以使用臨時變量、JS 自帶的流程控制、以及直接引用當前 JS 作用域中的值等等。
  • 開發工具對 JSX 的支持相比於現有可用的其他 Vue 模板還是比較先進的 (比如,linting、類型檢查、編輯器的自動完成)。

事實上 Vue 也提供了渲染函數,甚至支持 JSX。然而,我們默認推薦的還是模板。任何合乎規範的 HTML 都是合法的 Vue 模板,這也帶來了一些特有的優勢:

  • 對於很多習慣了 HTML 的開發者來說,模板比起 JSX 讀寫起來更自然。這裏當然有主觀偏好的成分,但如果這種區別會導致開發效率的提升,那麼它就有客觀的價值存在。
  • 基於 HTML 的模板使得將已有的應用逐步遷移到 Vue 更爲容易。
  • 這也使得設計師和新人開發者更容易理解和參與到項目中。
  • 你甚至可以使用其他模板預處理器,比如 Pug 來書寫 Vue 的模板。

有些開發者認爲模板意味着需要學習額外的 DSL (Domain-Specific Language 領域特定語言) 才能進行開發——我們認爲這種區別是比較膚淺的。首先,JSX 並不是免費的——它是基於 JS 之上的一套額外語法,因此也有它自己的學習成本。同時,正如同熟悉 JS 的人學習 JSX 會很容易一樣,熟悉 HTML 的人學習 Vue 的模板語法也是很容易的。最後,DSL 的存在使得我們可以讓開發者用更少的代碼做更多的事,比如 v-on 的各種修飾符,在 JSX 中實現對應的功能會需要多得多的代碼。

更抽象一點來看,我們可以把組件區分爲兩類:一類是偏視圖表現的 (presentational),一類則是偏邏輯的 (logical)。我們推薦在前者中使用模板,在後者中使用 JSX 或渲染函數。這兩類組件的比例會根據應用類型的不同有所變化,但整體來說我們發現表現類的組件遠遠多於邏輯類組件。

組件作用域內的 CSS

除非你把組件分佈在多個文件上 (例如 CSS Modules),CSS 作用域在 React 中是通過 CSS-in-JS 的方案實現的 (比如 styled-componentsglamorousemotion)。這引入了一個新的面向組件的樣式範例,它和普通的 CSS 撰寫過程是有區別的。另外,雖然在構建時將 CSS 提取到一個單獨的樣式表是支持的,但 bundle 裏通常還是需要一個運行時程序來讓這些樣式生效。當你能夠利用 JavaScript 靈活處理樣式的同時,也需要權衡 bundle 的尺寸和運行時的開銷。

如果你是一個 CSS-in-JS 的愛好者,許多主流的 CSS-in-JS 庫也都支持 Vue (比如 styled-components-vuevue-emotion)。這裏 React 和 Vue 主要的區別是,Vue 設置樣式的默認方法是單文件組件裏類似 style 的標籤。

單文件組件讓你可以在同一個文件裏完全控制 CSS,將其作爲組件代碼的一部分。

<style scoped>
  @media (min-width: 250px) {
    .list-container:hover {
      background: orange;
    }
  }
</style>

這個可選 scoped 屬性會自動添加一個唯一的屬性 (比如 data-v-21e5b78) 爲組件內 CSS 指定作用域,編譯的時候 .list-container:hover 會被編譯成類似 .list-container[data-v-21e5b78]:hover

最後,Vue 的單文件組件裏的樣式設置是非常靈活的。通過 vue-loader,你可以使用任意預處理器、後處理器,甚至深度集成 CSS Modules——全部都在 <style> 標籤內。

規模

向上擴展

Vue 和 React 都提供了強大的路由來應對大型應用。React 社區在狀態管理方面非常有創新精神 (比如 Flux、Redux),而這些狀態管理模式甚至 Redux 本身也可以非常容易的集成在 Vue 應用中。實際上,Vue 更進一步地採用了這種模式 (Vuex),更加深入集成 Vue 的狀態管理解決方案 Vuex 相信能爲你帶來更好的開發體驗。

兩者另一個重要差異是,Vue 的路由庫和狀態管理庫都是由官方維護支持且與核心庫同步更新的。React 則是選擇把這些問題交給社區維護,因此創建了一個更分散的生態系統。但相對的,React 的生態系統相比 Vue 更加繁榮。

最後,Vue 提供了 Vue-cli 腳手架,能讓你非常容易地構建項目,包含了 WebpackBrowserify,甚至 no build system。React 在這方面也提供了 create-react-app,但是現在還存在一些侷限性:

  • 它不允許在項目生成時進行任何配置,而 Vue 支持 Yeoman-like 定製。
  • 它只提供一個構建單頁面應用的單一模板,而 Vue 提供了各種用途的模板。
  • 它不能用用戶自建的模板構建項目,而自建模板對企業環境下預先建立協議是特別有用的。

而要注意的是這些限制是故意設計的,這有它的優勢。例如,如果你的項目需求非常簡單,你就不需要自定義生成過程。你能把它作爲一個依賴來更新。如果閱讀更多關於不同的設計理念

向下擴展

React 學習曲線陡峭,在你開始學 React 前,你需要知道 JSX 和 ES2015,因爲許多示例用的是這些語法。你需要學習構建系統,雖然你在技術上可以用 Babel 來實時編譯代碼,但是這並不推薦用於生產環境。

就像 Vue 向上擴展好比 React 一樣,Vue 向下擴展後就類似於 jQuery。你只要把如下標籤放到頁面就可以運行:

<script src="https://cdn.jsdelivr.net/npm/vue"></script>

然後你就可以編寫 Vue 代碼並應用到生產中,你只要用 min 版 Vue 文件替換掉就不用擔心其他的性能問題。

然後你就可以編寫 Vue 代碼並應用到生產中,你只要用 min 版 Vue 文件替換掉就不用擔心其他的性能問題。

由於起步階段不需學 JSX,ES2015 以及構建系統,所以開發者只需不到一天的時間閱讀指南就可以建立簡單的應用程序。

原生渲染

React Native 能使你用相同的組件模型編寫有本地渲染能力的 APP (iOS 和 Android)。能同時跨多平臺開發,對開發者是非常棒的。相應地,Vue 和 Weex 會進行官方合作,Weex 是阿里巴巴發起的跨平臺用戶界面開發框架,同時也正在 Apache 基金會進行項目孵化,Weex 允許你使用 Vue 語法開發不僅僅可以運行在瀏覽器端,還能被用於開發 iOS 和 Android 上的原生應用的組件。

在現在,Weex 還在積極發展,成熟度也不能和 React Native 相抗衡。但是,Weex 的發展是由世界上最大的電子商務企業的需求在驅動,Vue 團隊也會和 Weex 團隊積極合作確保爲開發者帶來良好的開發體驗。

另一個 Vue 的開發者們很快就會擁有的選項是 NativeScript,這是一個社區驅動的插件

MobX

Mobx 在 React 社區很流行,實際上在 Vue 也採用了幾乎相同的反應系統。在有限程度上,React + Mobx 也可以被認爲是更繁瑣的 Vue,所以如果你習慣組合使用它們,那麼選擇 Vue 會更合理。

AngularJS (Angular 1)

Vue 的一些語法和 AngularJS 的很相似 (例如 v-if vs ng-if)。因爲 AngularJS 是 Vue 早期開發的靈感來源。然而,AngularJS 中存在的許多問題,在 Vue 中已經得到解決。

複雜性

在 API 與設計兩方面上 Vue.js 都比 AngularJS 簡單得多,因此你可以快速地掌握它的全部特性並投入開發。

靈活性和模塊化

Vue.js 是一個更加靈活開放的解決方案。它允許你以希望的方式組織應用程序,而不是在任何時候都必須遵循 AngularJS 制定的規則,這讓 Vue 能適用於各種項目。我們知道把決定權交給你是非常必要的。
這也就是爲什麼我們提供 webpack template,讓你可以用幾分鐘,去選擇是否啓用高級特性,比如熱模塊加載、linting、CSS 提取等等。

數據綁定

AngularJS 使用雙向綁定,Vue 在不同組件間強制使用單向數據流。這使應用中的數據流更加清晰易懂。

指令與組件

在 Vue 中指令和組件分得更清晰。指令只封裝 DOM 操作,而組件代表一個自給自足的獨立單元——有自己的視圖和數據邏輯。在 AngularJS 中兩者有不少相混的地方。

運行時性能

Vue 有更好的性能,並且非常非常容易優化,因爲它不使用髒檢查。

在 AngularJS 中,當 watcher 越來越多時會變得越來越慢,因爲作用域內的每一次變化,所有 watcher 都要重新計算。並且,如果一些 watcher 觸發另一個更新,髒檢查循環 (digest cycle) 可能要運行多次。AngularJS 用戶常常要使用深奧的技術,以解決髒檢查循環的問題。有時沒有簡單的辦法來優化有大量 watcher 的作用域。

Vue 則根本沒有這個問題,因爲它使用基於依賴追蹤的觀察系統並且異步隊列更新,所有的數據變化都是獨立觸發,除非它們之間有明確的依賴關係。

有意思的是,Angular 和 Vue 用相似的設計解決了一些 AngularJS 中存在的問題。

Angular (原本的 Angular 2)

我們將新的 Angular 獨立開來討論,因爲它是一個和 AngularJS 完全不同的框架。例如:它具有優秀的組件系統,並且許多實現已經完全重寫,API 也完全改變了。

TypeScript

Angular 事實上必須用 TypeScript 來開發,因爲它的文檔和學習資源幾乎全部是面向 TS 的。TS 有很多好處——靜態類型檢查在大規模的應用中非常有用,同時對於 Java 和 C# 背景的開發者也是非常提升開發效率的。

然而,並不是所有人都想用 TS——在中小型規模的項目中,引入 TS 可能並不會帶來太多明顯的優勢。在這些情況下,用 Vue 會是更好的選擇,因爲在不用 TS 的情況下使用 Angular 會很有挑戰性。

最後,雖然 Vue 和 TS 的整合可能不如 Angular 那麼深入,我們也提供了官方的 類型聲明組件裝飾器,並且知道有大量用戶在生產環境中使用 Vue + TS 的組合。我們也和微軟的 TS / VSCode 團隊進行着積極的合作,目標是爲 Vue + TS 用戶提供更好的類型檢查和 IDE 開發體驗。

運行時性能

這兩個框架都很快,有非常類似的 benchmark 數據。你可以瀏覽具體的數據做更細粒度的對比,不過速度應該不是決定性的因素。

體積

在體積方面,最近的 Angular 版本中在使用了 AOTtree-shaking 技術後使得最終的代碼體積減小了許多。但即使如此,一個包含了 Vuex + Vue Router 的 Vue 項目 (gzip 之後 30kB) 相比使用了這些優化的 angular-cli 生成的默認項目尺寸 (~130kB) 還是要小得多。

靈活性

Vue 相比於 Angular 更加靈活,Vue 官方提供了構建工具來協助你構建項目,但它並不限制你去如何組織你的應用代碼。有人可能喜歡有嚴格的代碼組織規範,但也有開發者喜歡更靈活自由的方式。

學習曲線

要學習 Vue,你只需要有良好的 HTML 和 JavaScript 基礎。有了這些基本的技能,你就可以非常快速地通過閱讀 指南 投入開發。

Angular 的學習曲線是非常陡峭的——作爲一個框架,它的 API 面積比起 Vue 要大得多,你也因此需要理解更多的概念才能開始有效率地工作。當然,Angular 本身的複雜度是因爲它的設計目標就是隻針對大型的複雜應用;但不可否認的是,這也使得它對於經驗不甚豐富的開發者相當的不友好。

Ember

Ember 是一個全能框架。它提供了大量的約定,一旦你熟悉了它們,開發會變得很高效。不過,這也意味着學習曲線較高,而且並不靈活。這意味着在框架和庫 (加上一系列鬆散耦合的工具) 之間做權衡選擇。後者會更自由,但是也要求你做更多架構上的決定。

也就是說,我們最好比較的是 Vue 內核和 Ember 的模板數據模型層:

  • Vue 在普通 JavaScript 對象上建立響應,提供自動化的計算屬性。在 Ember 中需要將所有東西放在 Ember 對象內,並且手工爲計算屬性聲明依賴。
  • Vue 的模板語法可以用全功能的 JavaScript 表達式,而 Handlebars 的語法和幫助函數相比來說非常受限。
  • 在性能上,Vue 比 Ember 好很多,即使是 Ember 2.x 的最新 Glimmer 引擎。Vue 能夠自動批量更新,而 Ember 在性能敏感的場景時需要手動管理。
Knockout

Knockout 是 MVVM 領域內的先驅,並且追蹤依賴。它的響應系統和 Vue 也很相似。它在瀏覽器支持以及其他方面的表現也是讓人印象深刻的。它最低能支持到 IE6,而 Vue 最低只能支持到 IE9。

隨着時間的推移,Knockout 的發展已有所放緩,並且略顯有點老舊了。比如,它的組件系統缺少完備的生命週期事件方法,儘管這些在現在是非常常見的。以及相比於 Vue 調用子組件的接口它的方法顯得有點笨重。

如果你有興趣研究,你還會發現二者在接口設計的理念上是不同的。這可以通過各自創建的 simple Todo List 體現出來。或許有點主觀,但是很多人認爲 Vue 的 API 接口更簡單結構更優雅。

Polymer

Polymer 是另一個由谷歌贊助的項目,事實上也是 Vue 的一個靈感來源。Vue 的組件可以粗略的類比於 Polymer 的自定義元素,並且兩者具有相似的開發風格。最大的不同之處在於,Polymer 是基於最新版的 Web Components 標準之上,並且需要重量級的 polyfills 來幫助工作 (性能下降),瀏覽器本身並不支持這些功能。相比而言,Vue 在支持到 IE9 的情況下並不需要依賴 polyfills 來工作。

在 Polymer 1.0 版本中,爲了彌補性能,團隊非常有限的使用數據綁定系統。例如,在 Polymer 中唯一支持的表達式只有布爾值否定和單一的方法調用,它的 computed 方法的實現也並不是很靈活。

Polymer 自定義的元素是用 HTML 文件來創建的,這會限制使用 JavaScript/CSS (和被現代瀏覽器普遍支持的語言特性)。相比之下,Vue 的單文件組件允許你非常容易的使用 ES2015 和你想用的 CSS 預編譯處理器。

在部署生產環境時,Polymer 建議使用 HTML Imports 加載所有資源。而這要求服務器和客戶端都支持 Http 2.0 協議,並且瀏覽器實現了此標準。這是否可行就取決於你的目標用戶和部署環境了。如果狀況不佳,你必須用 Vulcanizer 工具來打包 Polymer 元素。而在這方面,Vue 可以結合異步組件的特性和 Webpack 的代碼分割特性來實現懶加載 (lazy-loaded)。這同時確保了對舊瀏覽器的兼容且又能更快加載。

而 Vue 和 Web Component 標準進行深層次的整合也是完全可行的,比如使用 Custom Elements、Shadow DOM 的樣式封裝。然而在我們做出嚴肅的實現承諾之前,我們目前仍在等待相關標準成熟,進而再廣泛應用於主流的瀏覽器中。

Riot

Riot 3.0 提供了一個類似於基於組件的開發模型 (在 Riot 中稱之爲 Tag),它提供了小巧精美的 API。Riot 和 Vue 在設計理念上可能有許多相似處。儘管相比 Riot ,Vue 要顯得重一點,Vue 還是有很多顯著優勢的:

  • 更好的性能。Riot 使用了 遍歷 DOM 樹 而不是虛擬 DOM,但實際上用的還是髒檢查機制,因此和 AngularJS 患有相同的性能問題。
  • 更多成熟工具的支持。Vue 提供官方支持 webpackBrowserify,而 Riot 是依靠社區來建立集成系統。

vue.js 2.x官方教程地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章