## 基本示例
這里有一個 Vue 組件的示例:
``` js
// 定義一個名為 button-counter 的新組件
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
```
組件是可復用的 Vue 實例,且帶有一個名字:在這個例子中是 `<button-counter>`。我們可以在一個通過 `new Vue` 創建的 Vue 根實例中,把這個組件作為自定義元素來使用:
```html
<div id="components-demo">
<button-counter></button-counter>
</div>
```
```js
new Vue({ el: '#components-demo' })
```
{% raw %}
<div id="components-demo" class="demo">
<button-counter></button-counter>
</div>
<script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count += 1">You clicked me {{ count }} times.</button>'
})
new Vue({ el: '#components-demo' })
</script>
{% endraw %}
因為組件是可復用的 Vue 實例,所以它們與 `new Vue` 接收相同的選項,例如 `data`、`computed`、`watch`、`methods` 以及生命周期鉤子等。僅有的例外是像 `el` 這樣根實例特有的選項。
## 組件的復用
你可以將組件進行任意次數的復用:
```html
<div id="components-demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
```
{% raw %}
<div id="components-demo2" class="demo">
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>
</div>
<script>
new Vue({ el: '#components-demo2' })
</script>
{% endraw %}
注意當點擊按鈕時,每個組件都會各自獨立維護它的 `count`。因為你每用一次組件,就會有一個它的新**實例**被創建。
### `data` 必須是一個函數
當我們定義這個 `<button-counter>` 組件時,你可能會發現它的 `data` 并不是像這樣直接提供一個對象:
```js
data: {
count: 0
}
```
取而代之的是,**一個組件的 `data` 選項必須是一個函數**,因此每個實例可以維護一份被返回對象的獨立的拷貝:
```js
data: function () {
return {
count: 0
}
}
```
如果 Vue 沒有這條規則,點擊一個按鈕就可能會像如下代碼一樣影響到*其它所有實例*:
{% raw %}
<div id="components-demo3" class="demo">
<button-counter2></button-counter2>
<button-counter2></button-counter2>
<button-counter2></button-counter2>
</div>
<script>
var buttonCounter2Data = {
count: 0
}
Vue.component('button-counter2', {
data: function () {
return buttonCounter2Data
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
new Vue({ el: '#components-demo3' })
</script>
{% endraw %}
## 組件的組織
通常一個應用會以一棵嵌套的組件樹的形式來組織:

例如,你可能會有頁頭、側邊欄、內容區等組件,每個組件又包含了其它的像導航鏈接、博文之類的組件。
為了能在模板中使用,這些組件必須先注冊以便 Vue 能夠識別。這里有兩種組件的注冊類型:**全局注冊**和**局部注冊**。至此,我們的組件都只是通過 `Vue.component` 全局注冊的:
```js
Vue.component('my-component-name', {
// ... options ...
})
```
全局注冊的組件可以用在其被注冊之后的任何 (通過 `new Vue`) 新創建的 Vue 根實例,也包括其組件樹中的所有子組件的模板中。
到目前為止,關于組件注冊你需要了解的就這些了,如果你閱讀完本頁內容并掌握了它的內容,我們會推薦你再回來把[組件注冊](components-registration.html)讀完。
## 通過 Prop 向子組件傳遞數據
早些時候,我們提到了創建一個博文組件的事情。問題是如果你不能向這個組件傳遞某一篇博文的標題或內容之類的我們想展示的數據的話,它是沒有辦法使用的。這也正是 prop 的由來。
Prop 是你可以在組件上注冊的一些自定義特性。當一個值傳遞給一個 prop 特性的時候,它就變成了那個組件實例的一個屬性。為了給博文組件傳遞一個標題,我們可以用一個 `props` 選項將其包含在該組件可接受的 prop 列表中:
```js
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
```
一個組件默認可以擁有任意數量的 prop,任何值都可以傳遞給任何 prop。在上述模板中,你會發現我們能夠在組件實例中訪問這個值,就像訪問 `data` 中的值一樣。
一個 prop 被注冊之后,你就可以像這樣把數據作為一個自定義特性傳遞進來:
```html
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
```
{% raw %}
<div id="blog-post-demo" class="demo">
<blog-post1 title="My journey with Vue"></blog-post1>
<blog-post1 title="Blogging with Vue"></blog-post1>
<blog-post1 title="Why Vue is so fun"></blog-post1>
</div>
<script>
Vue.component('blog-post1', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
new Vue({ el: '#blog-post-demo' })
</script>
{% endraw %}
然而在一個典型的應用中,你可能在 `data` 里有一個博文的數組:
```js
new Vue({
el: '#blog-post-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
})
```
并想要為每篇博文渲染一個組件:
```html
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
></blog-post>
```
如上所示,你會發現我們可以使用 `v-bind` 來動態傳遞 prop。這在你一開始不清楚要渲染的具體內容,比如[從一個 API 獲取博文列表](https://jsfiddle.net/chrisvfritz/sbLgr0ad)的時候,是非常有用的。
到目前為止,關于 prop 你需要了解的大概就這些了,如果你閱讀完本頁內容并掌握了它的內容,我們會推薦你再回來把 [prop](components-props.html) 讀完。
## 單個根元素
當構建一個 `<blog-post>` 組件時,你的模板最終會包含的東西遠不止一個標題:
```html
<h3>{{ title }}</h3>
```
最最起碼,你會包含這篇博文的正文:
```html
<h3>{{ title }}</h3>
<div v-html="content"></div>
```
然而如果你在模板中嘗試這樣寫,Vue 會顯示一個錯誤,并解釋道 **every component must have a single root element (每個組件必須只有一個根元素)**。你可以將模板的內容包裹在一個父元素內,來修復這個問題,例如:
```html
<div class="blog-post">
<h3>{{ title }}</h3>
<div v-html="content"></div>
</div>
```
看起來當組件變得越來越復雜的時候,我們的博文不只需要標題和內容,還需要發布日期、評論等等。為每個相關的信息定義一個 prop 會變得很麻煩:
```html
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
v-bind:content="post.content"
v-bind:publishedAt="post.publishedAt"
v-bind:comments="post.comments"
></blog-post>
```
所以是時候重構一下這個 `<blog-post>` 組件了,讓它變成接受一個單獨的 `post` prop:
```html
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
></blog-post>
```
```js
Vue.component('blog-post', {
props: ['post'],
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<div v-html="post.content"></div>
</div>
`
})
```
<p class="tip">上述的這個和一些接下來的示例使用了 JavaScript 的[模板字符串](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals)來讓多行的模板更易讀。它們在 IE 下并沒有被支持,所以如果你需要在不 (經過 Babel 或 TypeScript 之類的工具) 編譯的情況下支持 IE,請使用[折行轉義字符](https://css-tricks.com/snippets/javascript/multiline-string-variables-in-javascript/)取而代之。</p>
現在,不論何時為 `post` 對象添加一個新的屬性,它都會自動地在 `<blog-post>` 內可用。
## 通過事件向父級組件發送消息
在我們開發 `<blog-post>` 組件時,它的一些功能可能要求我們和父級組件進行溝通。例如我們可能會引入一個可訪問性的功能來放大博文的字號,同時讓頁面的其它部分保持默認的字號。
在其父組件中,我們可以通過添加一個 `postFontSize` 數據屬性來支持這個功能:
```js
new Vue({
el: '#blog-posts-events-demo',
data: {
posts: [/* ... */],
postFontSize: 1
}
})
```
它可以在模板中用來控制所有博文的字號:
```html
<div id="blog-posts-events-demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
></blog-post>
</div>
</div>
```
現在我們在每篇博文正文之前添加一個按鈕來放大字號:
```js
Vue.component('blog-post', {
props: ['post'],
template: `
<div class="blog-post">
<h3>{{ post.title }}</h3>
<button>
Enlarge text
</button>
<div v-html="post.content"></div>
</div>
`
})
```
問題是這個按鈕不會做任何事:
```html
<button>
Enlarge text
</button>
```
當點擊這個按鈕時,我們需要告訴父級組件放大所有博文的文本。幸好 Vue 實例提供了一個自定義事件的系統來解決這個問題。我們可以調用內建的 [**`$emit`** 方法](../api/#vm-emit)并傳入事件的名字,來向父級組件觸發一個事件:
```html
<button v-on:click="$emit('enlarge-text')">
Enlarge text
</button>
```
然后我們可以用 `v-on` 在博文組件上監聽這個事件,就像監聽一個原生 DOM 事件一樣:
```html
<blog-post
...
v-on:enlarge-text="postFontSize += 0.1"
></blog-post>
```
{% raw %}
<div id="blog-posts-events-demo" class="demo">
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:post="post"
v-on:enlarge-text="postFontSize += 0.1"
></blog-post>
</div>
</div>
<script>
Vue.component('blog-post', {
props: ['post'],
template: '\
<div class="blog-post">\
<h3>{{ post.title }}</h3>\
<button v-on:click="$emit(\'enlarge-text\')">\
Enlarge text\
</button>\
<div v-html="post.content"></div>\
</div>\
'
})
new Vue({
el: '#blog-posts-events-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue', content: '...content...' },
{ id: 2, title: 'Blogging with Vue', content: '...content...' },
{ id: 3, title: 'Why Vue is so fun', content: '...content...' }
],
postFontSize: 1
}
})
</script>
{% endraw %}
### 使用事件拋出一個值
有的時候用一個事件來拋出一個特定的值是非常有用的。例如我們可能想讓 `<blog-post>` 組件決定它的文本要放大多少。這時可以使用 `$emit` 的第二個參數來提供這個值:
```html
<button v-on:click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>
```
然后當在父級組件監聽這個事件的時候,我們可以通過 `$event` 訪問到被拋出的這個值:
```html
<blog-post
...
v-on:enlarge-text="postFontSize += $event"
></blog-post>
```
或者,如果這個事件處理函數是一個方法:
```html
<blog-post
...
v-on:enlarge-text="onEnlargeText"
></blog-post>
```
那么這個值將會作為第一個參數傳入這個方法:
```js
methods: {
onEnlargeText: function (enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
```
### 在組件上使用 `v-model`
自定義事件也可以用于創建支持 `v-model` 的自定義輸入組件。記住:
```html
<input v-model="searchText">
```
等價于:
```html
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
```
當用在組件上時,`v-model` 則會這樣:
``` html
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
```
為了讓它正常工作,這個組件內的 `<input>` 必須:
- 將其 `value` 特性綁定到一個名叫 `value` 的 prop 上
- 在其 `input` 事件被觸發時,將新的值通過自定義的 `input` 事件拋出
寫成代碼之后是這樣的:
```js
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
? ? ?v-on:input="$emit('input', $event.target.value)"
? ?>
`
})
```
現在 `v-model` 就應該可以在這個組件上完美地工作起來了:
```html
<custom-input v-model="searchText"></custom-input>
```
到目前為止,關于組件自定義事件你需要了解的大概就這些了,如果你閱讀完本頁內容并掌握了它的內容,我們會推薦你再回來把[自定義事件](components-custom-events.html)讀完。
## 通過插槽分發內容
和 HTML 元素一樣,我們經常需要向一個組件傳遞內容,像這樣:
``` html
<alert-box>
Something bad happened.
</alert-box>
```
可能會渲染出這樣的東西:
{% raw %}
<div id="slots-demo" class="demo">
<alert-box>
Something bad happened.
</alert-box>
</div>
<script>
Vue.component('alert-box', {
template: '\
<div class="demo-alert-box">\
<strong>Error!</strong>\
<slot></slot>\
</div>\
'
})
new Vue({ el: '#slots-demo' })
</script>
<style>
.demo-alert-box {
padding: 10px 20px;
background: #f3beb8;
border: 1px solid #f09898;
}
</style>
{% endraw %}
幸好,Vue 自定義的 `<slot>` 元素讓這變得非常簡單:
```js
Vue.component('alert-box', {
template: `
<div class="demo-alert-box">
<strong>Error!</strong>
<slot></slot>
</div>
`
})
```
如你所見,我們只要在需要的地方加入插槽就行了——就這么簡單!
到目前為止,關于插槽你需要了解的大概就這些了,如果你閱讀完本頁內容并掌握了它的內容,我們會推薦你再回來把[插槽](components-slots.html)讀完。
## 動態組件
有的時候,在不同組件之間進行動態切換是非常有用的,比如在一個多標簽的界面里:
{% raw %}
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
class="dynamic-component-demo-tab-button"
v-bind:class="{ 'dynamic-component-demo-tab-button-active': tab === currentTab }"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component
v-bind:is="currentTabComponent"
class="dynamic-component-demo-tab"
></component>
</div>
<script>
Vue.component('tab-home', { template: '<div>Home component</div>' })
Vue.component('tab-posts', { template: '<div>Posts component</div>' })
Vue.component('tab-archive', { template: '<div>Archive component</div>' })
new Vue({
el: '#dynamic-component-demo',
data: {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
},
computed: {
currentTabComponent: function () {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
</script>
<style>
.dynamic-component-demo-tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: -1px;
}
.dynamic-component-demo-tab-button:hover {
background: #e0e0e0;
}
.dynamic-component-demo-tab-button-active {
background: #e0e0e0;
}
.dynamic-component-demo-tab {
border: 1px solid #ccc;
padding: 10px;
}
</style>
{% endraw %}
上述內容可以通過 Vue 的 `<component>` 元素加一個特殊的 `is` 特性來實現:
```html
<!-- 組件會在 `currentTabComponent` 改變時改變 -->
<component v-bind:is="currentTabComponent"></component>
```
在上述示例中,`currentTabComponent` 可以包括
- 已注冊組件的名字,或
- 一個組件的選項對象
你可以在[這里](https://jsfiddle.net/chrisvfritz/o3nycadu/)查閱并體驗完整的代碼,或在[這個版本](https://jsfiddle.net/chrisvfritz/b2qj69o1/)了解綁定組件選項對象,而不是已注冊組件名的示例。
到目前為止,關于動態組件你需要了解的大概就這些了,如果你閱讀完本頁內容并掌握了它的內容,我們會推薦你再回來把[動態和異步組件](components-dynamic-async.html)讀完。
## 解析 DOM 模板時的注意事項
有些 HTML 元素,諸如 `<ul>`、`<ol>`、`<table>` 和 `<select>`,對于哪些元素可以出現在其內部是有嚴格限制的。而有些元素,諸如 `<li>`、`<tr>` 和 `<option>`,只能出現在其它某些特定的元素內部。
這會導致我們使用這些有約束條件的元素時遇到一些問題。例如:
``` html
<table>
<blog-post-row></blog-post-row>
</table>
```
這個自定義組件 `<blog-post-row>` 會被作為無效的內容提升到外部,并導致最終渲染結果出錯。幸好這個特殊的 `is` 特性給了我們一個變通的辦法:
``` html
<table>
<tr is="blog-post-row"></tr>
</table>
```
需要注意的是**如果我們從以下來源使用模板的話,這條限制是*不存在*的**:
- 字符串 (例如:`template: '...'`)
- [單文件組件 (`.vue`)](single-file-components.html)
- [`<script type="text/x-template">`](components-edge-cases.html#X-Templates)
到這里,你需要了解的解析 DOM 模板時的注意事項——實際上也是 Vue 的全部*必要內容*,大概就是這些了。恭喜你!接下來還有很多東西要去學習,不過首先,我們推薦你先休息一下,試用一下 Vue,自己隨意做些好玩的東西。
如果你感覺已經掌握了這些知識,我們推薦你再回來把完整的組件指南,包括側邊欄中組件深入章節的所有頁面讀完。
- 寫在前面
- 基礎
- 安裝
- 介紹
- Vue實例
- 模板語法
- 計算屬性和偵聽器
- Class 與 Style 綁定
- 條件渲染
- 列表渲染
- 事件處理
- 表單輸入綁定
- 組件基礎
- 深入了解組件
- 組件注冊
- Prop
- 自定義事件
- 插槽
- 動態組件 & 異步組件
- 處理邊界情況
- 過渡 & 動畫
- 進入/離開 & 列表過渡
- 狀態過渡
- 可復用性 & 組合
- 混入
- 自定義指令
- 渲染函數 & JSX
- 插件
- 過濾器
- 工具
- 生產環境部署
- 單文件組件
- 單元測試
- TypeScript 支持
- 規模化
- 路由
- 狀態管理
- 服務端渲染
- 內在
- 深入響應式原理
- 遷移
- 從 Vue 1.x 遷移
- 從 Vue Router 0.7.x 遷移
- 從 Vuex 0.6.x 遷移到 1.0
- 更多
- 對比其他框架
- 加入 Vue.js 社區
- 開發團隊