組件能夠擴展 HTML 元素,封裝可重用的代碼javascript
在較高層面上,組件是自定義元素, Vue.js 的編譯器爲它添加特殊功能html
在有些狀況下,組件也能夠是原生 HTML 元素的形式,以 is 特性擴展。vue
<div id="example"> <!--web組件的定義脫離了通常的dom元素的寫法,至關於自定義了元素--> <my-component></my-component> </div>
// 註冊全局組件,指定以前設定的元素名,而後傳入對象 Vue.component('my-component', { template: '<div>A custom component!</div>' }) // 建立根實例 new Vue({ el: '#example' })
沒必要在全局註冊每一個組件。經過使用組件實例選項註冊,可使組件僅在另外一個實例/組件的做用域中可用java
//將傳入給組件的對象單獨寫 var Child = { template: '<div>A custom component!</div>' } new Vue({ //經過components語法建立局部組件 //將組件僅僅放在這個vue實例裏面使用 components: { // <my-component> 將只在父模板可用 'my-component': Child } })
當使用 DOM 做爲模版時(例如,將 el 選項掛載到一個已存在的元素上), 你會受到 HTML 的一些限制,jquery
由於 Vue 只有在瀏覽器解析和標準化 HTML 後才能獲取模版內容。
尤爲像這些元素 <ul> , <ol>, <table> , <select> 限制了能被它包裹的元素, <option> 只能出如今其它元素內部。web
<!--這種是不行的,會報錯--> <table> <my-row>...</my-row> </table> <!--要經過is屬性來處理--> <table> <tr is="my-row"></tr> </table>
使用組件時,大多數能夠傳入到 Vue 構造器中的選項能夠在註冊組件時使用,有一個例外: data 必須是函數。 實際上chrome
//這樣會報錯,提示data必須是一個函數 Vue.component('my-component', { template: '<span>{{ message }}</span>', data: { message: 'hello' } })
<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' })
避免出現同時改變數據的狀況api
//返回一個新的對象,而不是返回同一個data對象引用 data: function () { return { //字面量寫法會建立新對象 counter: 0 } }
組件意味着協同工做,一般父子組件會是這樣的關係:數組
組件 A 在它的模版中使用了組件 B 。它們之間必然須要相互通訊瀏覽器
父組件要給子組件傳遞數據,子組件須要將它內部發生的事情告知給父組件
然而,在一個良好定義的接口中儘量將父子組件解耦是很重要的。這保證了每一個組件能夠在相對隔離的環境中書寫和理解,也大幅提升了組件的可維護性和可重用性。
在 Vue.js 中,父子組件的關係能夠總結爲 props down, events up 。
父組件經過 props 向下傳遞數據給子組件,子組件經過 events 給父組件發送消息。看看它們是怎麼工做的。
使用prop傳遞數據
組件實例的做用域是孤立的。這意味着不能而且不該該在子組件的模板內直接引用父組件的數據。
使用 props 把數據傳給子組件。
prop 是父組件用來傳遞數據的一個自定義屬性
子組件須要顯式地用 props 選項聲明 「prop」
<div id="example-2"> <!--向這個組件傳入一個字符串--> <child message="hello!"></child> </div>
Vue.component('child', { // 聲明 props,用數組形式的對象 props: ['message'], // 就像 data 同樣,prop 能夠用在模板內 // 一樣也能夠在 vm 實例中像 「this.message」 這樣使用 template: '<span>{{ message }}</span>' }); new Vue({ el: '#example-2' })
用 v-bind 動態綁定 props 的值到父組件的數據中。每當父組件的數據變化時,該變化也會傳導給子組件
<div id="example-2"> <!--使用v-modal實現雙向綁定--> <input v-model="parentMsg"> <br> <!--須要注意這裏使用短橫線的變量,由於在html下是使用短橫線變量的,可是在vue下使用駝峯變量--> <!--將父組件的parentMsg和子組件的my-message進行綁定--> <child v-bind:my-message="parentMsg"></child> </div>
Vue.component('child', { // 聲明 props props: ['my-message'], template: '<span>{{ myMessage }}</span>' //若是寫my-message會報錯,須要轉換爲駝峯寫法 }); new Vue({ el: '#example-2', data: { parentMsg: '' } })
HTML 特性不區分大小寫。當使用非字符串模版時,prop的名字形式會從 camelCase 轉爲 kebab-case(短橫線隔開)
在javascript裏面使用駝峯寫法,可是在html裏面須要轉成短橫線寫法
反之亦然,vue會自動處理來自html的短橫線寫法轉爲駝峯寫法
<!-- 默認只傳遞了一個字符串"1" --> <comp some-prop="1"></comp> <!-- 用v-bind實現傳遞實際的數字 --> <comp v-bind:some-prop="1"></comp>
prop 是單向綁定的
當父組件的屬性變化時,將傳導給子組件,可是不會反過來。這是爲了防止子組件無心修改了父組件的狀態——這會讓應用的數據流難以理解。
每次父組件更新時,子組件的全部 prop 都會更新爲最新值。這意味着你不該該在子組件內部改變 prop 。若是你這麼作了,Vue 會在控制檯給出警告。
一般有兩種改變 prop 的狀況:
prop 做爲初始值傳入,子組件以後只是將它的初始值做爲本地數據的初始值使用
定義一個局部 data 屬性,並將 prop 的初始值做爲局部數據的初始值。
<div id="example-2"> <!--這裏用短橫線寫法--> <child initial-counter="10"></child> </div>
Vue.component('child', { props: ['initialCounter'],//這裏用駝峯寫法 data: function () { //轉爲一個局部變量,寫一個data對象給組件使用 return {counter: this.initialCounter} }, template: '<span>{{ counter }}</span>' }); new Vue({ el: '#example-2' })
prop 做爲須要被轉變的原始值傳入。
定義一個 computed 屬性,此屬性從 prop 的值計算得出。
//例子沒有寫完,可是根據第一個例子能夠知道利用computed的手法原理其實跟寫一個data差很少 props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
注意在 JavaScript 中對象和數組是引用類型,指向同一個內存空間,若是 prop 是一個對象或數組,在子組件內部改變它會影響父組件的狀態。
組件能夠爲 props 指定驗證要求,當組件給其餘人使用時這頗有用。
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 } } } })
每一個 Vue 實例都實現了事件接口(Events interface)
使用 $on(eventName) 監聽事件
使用 $emit(eventName) 觸發事件
父組件能夠在使用子組件的地方直接用 v-on 來監聽子組件觸發的事件。
<div id="counter-event-example"> <p>{{ total }}</p> <!--監聽子組件的事件觸發,監聽increment1事件,處理程序爲incrementTotal事件--> <button-counter v-on:increment1="incrementTotal"></button-counter> <!--關鍵在於這裏v-on綁定的是一個子組件的事件,而且賦值了一個父組件的方法給他,那麼子組件裏面就可使用這個方法--> <button-counter v-on:increment1="incrementTotal"></button-counter> </div>
Vue.component('button-counter', { //監聽click事件,處理程序爲increment(子組件定義的方法) template: '<button v-on:click="increment">{{ counter }}</button>', //每個counter都是獨立的對象屬性 data: function () { return { counter: 0 } }, //子組件的方法 methods: { increment: function () { this.counter += 1; //在子組件裏面直接觸發以前監聽的increment1事件來執行父組件的方法 this.$emit('increment1'); } }, }) new Vue({ el: '#counter-event-example', data: { total: 0 }, //父組件的方法 methods: { incrementTotal: function () { this.total += 1 } } })
1.組件之間由於做用域不一樣的關係,因此相互獨立,因此子組件想要使用父組件的方法的話須要作一個新的監聽映射
<!--代替.on,這麼就可以綁定原生js的事件了--> <my-component v-on:click.native="doTheThing"></my-component>
自定義事件也能夠用來建立自定義的表單輸入組件,使用 v-model 來進行數據雙向綁定。
因此要讓組件的 v-model 生效,它必須:
接受一個 value 屬性
在有新的 value 時觸發 input 事件
<!--直接使用v-model,v-modal默認處理input事件--> <input v-model="something"> <!--v-modal是語法糖,翻譯過來原理是這樣:--> <!--綁定一個value,而後監聽input事件,經過獲取input的輸入來不斷改變綁定的value的值,知足了v-modal的觸發條件就能夠實現v-modal了--> <input v-bind:value="something" v-on:input="something = $event.target.value">
一個很是簡單的貨幣輸入:
<!--綁定一個v-model爲price,實際上是綁定了一個value--> <currency-input v-model="price"></currency-input>
Vue.component('currency-input', { template: '\ <span>\ $\ <input\ ref="input"\ //註冊爲input,是DOM的節點元素 v-bind:value="value"\ //v-model的value(也是prop) v-on:input="updateValue($event.target.value)"\ //封裝更新value的函數 >\ </span>\ ', props: ['value'], //父組件將綁定的value傳給子組件 methods: { // 不是直接更新值,而是使用此方法來對輸入值進行格式化和位數限制 updateValue: function (value) { var formattedValue = value //對值進行處理 // 刪除兩側的空格符 .trim() // 保留 2 小數位和2位數 .slice(0, value.indexOf('.') + 3) // 若是值不統一,手動覆蓋以保持一致,爲了保持輸入框顯示內容跟格式化內容一致 if (formattedValue !== value) { //由於註冊是一個input元素,因此this.$refs 就是input元素 this.$refs.input.value = formattedValue } //手動觸發input事件,將格式化後的值傳過去,這是最終顯示輸入框的輸出 this.$emit('input', Number(formattedValue)) } } }) //實例化vue實例的 new Vue({ el: '#aa', //要綁定一個vue實例,例如包裹一個id爲aa的div data:{ price:'' //v-model要有數據源 } })
ref 被用來給元素或子組件註冊引用信息。引用信息會根據父組件的 $refs 對象進行註冊。若是在普通的DOM元素上使用,引用信息就是元素; 若是用在子組件上,引用信息就是組件實例 ref
這是一個比較完整的例子:
<div id="app"> <!--有3個組件,分別不一樣的v-model--> <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"\ // 這些沒什麼特別,引用註冊爲input DOM元素 v-bind:value="value"\ v-on:input="updateValue($event.target.value)"\ v-on:focus="selectAll"\ //這裏多了focus事件監聽,焦點在的時候全選,也只是多了處理而已,對總體邏輯理解沒啥影響 v-on:blur="formatValue"\ //這裏多了blur事件監聽,焦點離開的時候格式化 >\ </div>\ ', props: { //多個prop傳遞,由於prop是對象,只要是對象格式就行 value: { type: Number, default: 0 }, label: { type: String, default: '' } }, mounted: function () { //這是vue的過渡狀態,暫時忽略不影響理解 this.formatValue() }, methods: { updateValue: function (value) { var result = currencyValidator.parse(value, this.value) if (result.warning) { // 這裏也使用了$refs獲取引用註冊信息 this.$refs.input.value = result.value } this.$emit('input', result.value) }, formatValue: function () { this.$refs.input.value = currencyValidator.format(this.value) //這裏注意下,這個this是prop傳遞過來的,也至關於這個組件做用域 }, selectAll: function (event) { //event能夠獲取原生的js事件 // 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) } } })
在簡單的場景下,使用一個空的 Vue 實例做爲中央事件總線:
var bus = new Vue() // 觸發組件 A 中的事件 bus.$emit('id-selected', 1) /* 經過on來監聽子組件的事件來實現傳遞 */ // 在組件 B 建立的鉤子中監聽事件 bus.$on('id-selected', function (id) { // ... })
爲了讓組件能夠組合,咱們須要一種方式來混合父組件的內容與子組件本身的模板。這個過程被稱爲 內容分發 (或 「transclusion」 若是你熟悉 Angular)
組件做用域簡單地說是:父組件模板的內容在父組件做用域內編譯;子組件模板的內容在子組件做用域內編譯。
假定 someChildProperty 是子組件的屬性,上例不會如預期那樣工做。父組件模板不該該知道子組件的狀態。
<!-- 無效 --> <child-component v-show="someChildProperty"></child-component>
若是要綁定子組件內的指令到一個組件的根節點,應當在它的模板內這麼作:
Vue.component('child-component', { // 有效,由於是在正確的做用域內 template: '<div v-show="someChildProperty">Child</div>', data: function () { return { //由於這個屬性在當前組件內編譯(建立了) someChildProperty: true } } })
相似地,分發內容是在父組件做用域內編譯。
除非子組件模板包含至少一個 <slot> 插口,不然父組件的內容將會被丟棄。
當子組件模板只有一個沒有屬性的 slot 時,父組件整個內容片斷將插入到 slot 所在的 DOM 位置,並替換掉 slot 標籤自己。
備用內容在子組件的做用域內編譯,而且只有在宿主元素爲空,且沒有要插入的內容時才顯示備用內容。
<!--父組件模版:--> <div id="aa"> <h1>我是父組件的標題</h1> <!--子組件的做用域內編譯,宿主元素爲空,且沒有要插入的內容--> <my-component></my-component> <my-component> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </my-component> </div>
Vue.component('my-component', { //my-component 組件有下面模板 template: '\ <div>\ <h2>我是子組件的標題</h2> \ <slot> \ //有slot插口,因此沒有被父組件丟棄 只有在沒有要分發的內容時纔會顯示。\ </slot> \ </div> \ ' }) new Vue({ el: '#aa', })
渲染結果:
<div id="aa"><h1>我是父組件的標題</h1> <div> <h2>我是子組件的標題</h2> <!--這裏是直接插入,沒有使用DOM元素--> 只有在沒有要分發的內容時纔會顯示。 </div> <div> <h2>我是子組件的標題</h2> <p>這是一些初始內容</p> <p>這是更多的初始內容</p> </div> </div>
<slot> 元素能夠用一個特殊的屬性 name 來配置如何分發內容。多個 slot 能夠有不一樣的名字。具名 slot 將匹配內容片斷中有對應 slot 特性的元素。
仍然能夠有一個匿名 slot ,它是默認 slot ,做爲找不到匹配的內容片斷的備用插槽。若是沒有默認的 slot ,這些找不到匹配的內容片斷將被拋棄。
<div id="aa"> <app-layout> <!--這是header--> <h1 slot="header">這裏多是一個頁面標題</h1> <p>主要內容的一個段落。</p> <p>另外一個主要段落。</p> <!--這是footer--> <p slot="footer">這裏有一些聯繫信息</p> </app-layout> </div>
Vue.component('app-layout', { template: '\ <div class="container"> \ <header> \ //找到名字叫header的slot以後替換內容,這裏替換的是整個DOM <slot name="header"></slot> \ </header> \ <main> \ //由於slot沒有屬性,會將內容插入到slot的所在的DOM位置 <slot></slot> \ </main> \ <footer>\ //跟header相似 <slot name="footer"></slot> \ </footer> \ </div> \ ' }); new Vue({ el: '#aa', })
渲染結果爲:
<div class="container"> <header> <h1>這裏多是一個頁面標題</h1> </header> <main> <p>主要內容的一個段落。</p> <p>另外一個主要段落。</p> </main> <footer> <p>這裏有一些聯繫信息</p> </footer> </div>
做用域插槽是一種特殊類型的插槽,用做使用一個(可以傳遞數據到)可重用模板替換已渲染元素。
在子組件中,只需將數據傳遞到插槽,就像你將 prop 傳遞給組件同樣
在父級中,具備特殊屬性 scope 的 <template> 元素,表示它是做用域插槽的模板。scope 的值對應一個臨時變量名,此變量接收從子組件中傳遞的 prop 對象
<div id="parent" class="parent"> <child> <!--接收從子組件中傳遞的prop對象(這個就是做用域插槽)--> <template scope="props"> <span>hello from parent</span> <!--使用這個prop對象--> <span>{{ props.text }}</span> </template> </child> </div>
Vue.component('child', { props: ['props'], //這個寫不寫均可以,做用域插槽固定會接收prop對象,並且這個prop對象是確定存在的 template: '\ <div class="child"> \ <slot text="hello from child"></slot> \ //在子組件裏直接將數據傳遞給slot </div> \ ' }); new Vue({ el: '#parent', })
渲染結果:
<div class="parent"> <div class="child"> <span>hello from parent</span> <!--子組件的東西出如今這裏了--> <span>hello from child</span> </div> </div>
另一個例子,做用域插槽更具表明性的用例是列表組件,容許組件自定義應該如何渲染列表每一項
<div id="parent"> <!--綁定一個組件的prop ,位置1--> <my-awesome-list :items="items"> <!-- 做用域插槽也能夠在這裏命名 --> <!--這裏props只表明肯定接受prop對象的東西,不關注prop對象裏面有什麼,位置2--> <template slot="item" scope="props"> <li class="my-fancy-item">{{ props.text }}</li> </template> </my-awesome-list> </div>
Vue.component('my-awesome-list', { props:['items'], //須要聲明prop爲items,須要是爲下面的循環遍歷的items的數據源作設定,位置3 template: '\ <ul> \ <slot name="item" v-for="item in items" :text="item.text"> \ //在slot中,循環遍歷輸出items的text,位置4 </slot> \ </ul> \ ' }); new Vue({ el: '#parent', data : { items:[ //初始化items數據 {text:"aa"}, {text:"bb"} ] } })
位置1,實現了一個組件的prop綁定,prop須要在組件裏面聲明,這裏綁定的是items,這是要將父組件的items傳遞到子組件,因此在位置3裏面須要聲明,在vue實例要初始化
位置2,這裏scope的props是表明做用域插槽接收來自prop對象的數據,props.text是表明每個li要輸出的是prop對象的text屬性
位置3,在組件裏聲明props,爲了接收父組件綁定的items屬性,而後將其給位置4的循環使用
位置4,這裏綁定了text屬性,就是前呼位置2裏面輸出的prop對象的text屬性
多個組件可使用同一個掛載點,而後動態地在它們之間切換。使用保留的 <component> 元素,動態地綁定到它的 is 特性
var vm = new Vue({ el: '#example', data: { currentView: 'home' //默認值 }, components: { //根據不一樣的值進行不一樣的組件切換,這裏用components寫法 home: { /* ... */ }, posts: { /* ... */ }, archive: { /* ... */ } } })
<!--這個is是一個字符串,根據返回值來給組件進行v-bind--> <component v-bind:is="currentView"> <!-- 組件在 vm.currentview 變化時改變! --> </component>
若是把切換出去的組件保留在內存中,能夠保留它的狀態或避免從新渲染。爲此能夠添加一個 keep-alive 指令參數
<keep-alive> <component :is="currentView"> <!-- 非活動組件將被緩存! --> </component> </keep-alive>
在編寫組件時,記住是否要複用組件有好處。一次性組件跟其它組件緊密耦合不要緊,可是可複用組件應當定義一個清晰的公開接口。
Vue 組件的 API 來自三部分 - props, events 和 slots :
Props 容許外部環境傳遞數據給組件
Events 容許組件觸發外部環境的反作用
Slots 容許外部環境將額外的內容組合在組件中。
<!--v-bind,縮寫:,綁定prop--> <!--v-on,縮寫@,監聽事件--> <!--slot插槽--> <my-component :foo="baz" :bar="qux" @event-a="doThis" @event-b="doThat" > <img slot="icon" src="..."> <p slot="main-text">Hello!</p> </my-component>
儘管有 props 和 events ,可是有時仍然須要在 JavaScript 中直接訪問子組件。爲此可使用 ref 爲子組件指定一個索引 ID 。
<div id="parent"> <user-profile ref="profile"></user-profile> </div>
var parent = new Vue({ el: '#parent' }) // 訪問子組件 var child = parent.$refs.profile
當 ref 和 v-for 一塊兒使用時, ref 是一個數組或對象,包含相應的子組件。
$refs 只在組件渲染完成後才填充,而且它是非響應式的。它僅僅做爲一個直接訪問子組件的應急方案——應當避免在模版或計算屬性中使用 $refs 。
ref 被用來給元素或子組件註冊引用信息。引用信息會根據父組件的 $refs 對象進行註冊。若是在普通的DOM元素上使用,引用信息就是元素; 若是用在子組件上,引用信息就是組件實例 ref
當註冊組件(或者 props)時,可使用 kebab-case ,camelCase ,或 TitleCase 。Vue 不關心這個。
在 HTML 模版中,請使用 kebab-case 形式:
// 在組件定義中 components: { // 使用 kebab-case 形式註冊--橫線寫法 'kebab-cased-component': { /* ... */ }, // register using camelCase --駝峯寫法 'camelCasedComponent': { /* ... */ }, // register using TitleCase --標題寫法 'TitleCasedComponent': { /* ... */ } }
<!-- 在HTML模版中始終使用 kebab-case--橫線寫法 --> <kebab-cased-component></kebab-cased-component> <camel-cased-component></camel-cased-component> <title-cased-component></title-cased-component>
組件在它的模板內能夠遞歸地調用本身,不過,只有當它有 name 選項時才能夠
當你利用Vue.component全局註冊了一個組件, 全局的ID做爲組件的 name 選項,被自動設置.
//組件能夠用name來寫名字 name: 'unique-name-of-my-component' //也能夠在建立的時候默認添加名字 Vue.component('unique-name-of-my-component', { // ... }) //若是同時使用的話,遞歸的時候就會不斷遞歸本身,致使溢出 name: 'stack-overflow', template: '<div><stack-overflow></stack-overflow></div>'
儘管在 Vue 中渲染 HTML 很快,不過當組件中包含大量靜態內容時,能夠考慮使用 v-once 將渲染結果緩存起來,就像這樣:
Vue.component('terms-of-service', { template: '\ <div v-once>\ <h1>Terms of Service</h1>\ ... a lot of static content ...\ </div>\ ' })
v-once只渲染元素和組件一次。隨後的從新渲染,元素/組件及其全部的子節點將被視爲靜態內容並跳過。這能夠用於優化更新性能。