本章將繼續和大家分享Vue的一些基礎知識。話不多說,下麵我們直接上代碼: 本文內容大部分摘自Vue的官網:https://v2.cn.vuejs.org/v2/guide/ 首先我們先來看一下Demo的目錄結構,如下所示: 一、偵聽器 <!DOCTYPE html> <html lang="en"> ...
本章將繼續和大家分享Vue的一些基礎知識。話不多說,下麵我們直接上代碼:
本文內容大部分摘自Vue的官網:https://v2.cn.vuejs.org/v2/guide/
首先我們先來看一下Demo的目錄結構,如下所示:
一、偵聽器
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue中的偵聽器</title> <script src="/js/lib/vue.js"></script> <script src="/js/lib/axios.js"></script> <script src="/js/lib/lodash.js"></script> </head> <body> <div id="app"> <div desc="偵聽屬性"> <p> 請輸入您的問題: <input v-model="question"> </p> <p>{{ answer }}</p> </div> </div> <script> var vm = new Vue({ el: '#app', //掛載點 data: { question: '', answer: '在您提出問題之前,我不能給您答案!', student: { name: '張三', age: 18 } }, watch: { //簡單監聽 //如果 `question` 發生改變,這個函數就會運行 question: function (newQuestion, oldQuestion) { var _this = this; _this.answer = '正在等待您停止輸入...' _this.debouncedGetAnswer(); }, //對對象進行深度監聽 //普通的watch方法無法監聽到對象內部屬性的變化 student: { handler(newValue, oldValue) { // 註意:在嵌套的變更中, // 只要沒有替換對象本身, // 那麼這裡的 `newValue` 和 `oldValue` 相同,都是新值 console.log(newValue); console.log(oldValue); }, deep: true, // 深度監聽 immediate: true // 強制立即執行回調(一般用於父組件向子組件動態傳值時) }, //對對象的某一個屬性進行深度監聽 //如果想要監聽對象的某一個屬性,並且希望獲取該屬性變化前後的值則需要用該方式進行監聽 'student.age': { handler(newValue, oldValue) { console.log(newValue); console.log(oldValue); }, deep: true, // 深度監聽 immediate: true // 強制立即執行回調(一般用於父組件向子組件動態傳值時) } }, created: function () { // `_.debounce` 是一個通過 Lodash 限制操作頻率的函數。 // 在這個例子中,我們希望限制訪問 介面 的頻率 // AJAX 請求直到用戶輸入完畢才會發出。想要瞭解更多關於 // `_.debounce` 函數 (及其近親 `_.throttle`) 的知識, // 請參考:https://lodash.com/docs#debounce var _this = this; _this.debouncedGetAnswer = _.debounce(_this.getAnswer, 1500); //debouncedGetAnswer 方法名可自定義 }, methods: { getAnswer: function () { var _this = this; if (_this.question.indexOf('?') === -1) { _this.answer = '問題通常包含問號!'; return; } _this.answer = '數據獲取中...'; axios.get('https://autumnfish.cn/api/joke') .then(function (response) { _this.answer = response.data; }) .catch(function (error) { _this.answer = '請求介面異常:' + error; }); } } }); </script> </body> </html>
二、Vue組件基礎
自定義組件 <button-counter> 代碼如下:
define([ 'axios' ], function (axios) { /* 因為組件是可復用的 Vue 實例,所以它們與 new Vue 接收相同的選項,例如 data、computed、watch、methods 以及生命周期鉤子等。 僅有的例外是像 el 這樣根實例特有的選項。 */ return { template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>', props: [], //一個組件的 data 選項必須是一個函數,因此每個實例可以維護一份被返回對象的獨立的拷貝 data: function () { return { count: 0 } }, mounted: function () { }, methods: { }, watch: { } }; });
自定義組件 <blog-post> 代碼如下:
define([ 'axios' ], function (axios) { /* 因為組件是可復用的 Vue 實例,所以它們與 new Vue 接收相同的選項,例如 data、computed、watch、methods 以及生命周期鉤子等。 僅有的例外是像 el 這樣根實例特有的選項。 */ return { //每個組件必須只有一個根元素 template: ` <div class="blog-post" desc="根元素"> <h3>{{ post.title }}</h3> <p>子組件中的titleNew:<input type="text" v-model="titleNew"></p> <slot></slot> <button @click="handleEnlargeText">Enlarge text</button> <div v-html="post.content"></div> </div> `, /* 1、通過 Prop 向子組件傳遞數據。 2、一個組件預設可以擁有任意數量的 prop,任何值都可以傳遞給任何 prop。 在上述模板中,你會發現我們能夠在組件實例中訪問這個值,就像訪問 data 中的值一樣。 3、所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。 這樣會防止從子組件意外變更父級組件的狀態,從而導致你的應用的數據流向難以理解。 */ props: ['post', 'title'], //一個組件的 data 選項必須是一個函數,因此每個實例可以維護一份被返回對象的獨立的拷貝 data: function () { return { enlargeFontSize: 0.1, //需要放大字體的大小 titleNew: this.title, //初始值為props中父組件傳遞過來的值 } }, mounted: function () { }, methods: { //處理放大文本字體 handleEnlargeText: function () { var _this = this; //子組件可以通過調用內建的 $emit 方法並傳入事件名稱來觸發一個父組件的事件 //第二個參數為調用父組件事件所需傳的參數 //enlarge-text為自定義事件 _this.$emit('enlarge-text', _this.enlargeFontSize); } }, watch: { //監聽器完整寫法 title: { handler(newValue, oldValue) { this.titleNew = newValue; }, //deep: true, // 深度監聽 immediate: true // 強制立即執行回調(一般用於父組件向子組件動態傳值時) }, //監聽器簡寫,當需要設置 deep 或者 immediate 時需使用完整寫法 titleNew: function (newValue, oldValue) { /* 註意在 JavaScript 中對象和數組是通過引用傳入的,所以對於一個數組或對象類型的 prop 來說, 在子組件中改變變更這個對象或數組本身將會影響到父組件的狀態。 這種情況下就不需要以 update:myPropName 的模式觸發更新事件了。 */ this.$emit('update:title', newValue); //更新父組件title屬性綁定的值 /* 自定義事件 .sync 修飾符: 在有些情況下,我們可能需要對一個 prop 進行“雙向綁定”。 不幸的是,真正的雙向綁定會帶來維護上的問題,因為子組件可以變更父組件,且在父組件和子組件兩側都沒有明顯的變更來源。 這也是為什麼我們推薦以 update:myPropName 的模式觸發事件取而代之。 舉個例子,在一個包含 title prop 的假設的組件中,我們可以用以下方法表達對其賦新值的意圖: this.$emit('update:title', newTitle) 然後父組件可以監聽那個事件並根據需要更新一個本地的數據 property。例如: <text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event"> </text-document> 為了方便起見,我們為這種模式提供一個縮寫,即 .sync 修飾符: <text-document :title.sync="doc.title"></text-document> */ } } }; });
Vue組件基礎.html 代碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue組件基礎</title> </head> <body> <div id="app"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> <div :style="{ fontSize: postFontSize + 'em' }"> <blog-post v-for="post in posts" :key="post.id" :title.sync="post.title" :post="post" v-on:enlarge-text="onEnlargeText"> <!-- 預設插槽的內容 --> <template v-slot:default> <p>父組件中的post.title:<input type="text" v-model="post.title" /></p> </template> </blog-post> </div> </div> <script src="/js/lib/require.js"></script> <script src="/js/common/require_config.js"></script> <script src="/js/ComponentsDemo.js"></script> </body> </html>
其中 ComponentsDemo.js 代碼如下:
//Vue組件基礎 require(['../common/base', '../components/blogPost'], function (base, blogPost) { let axios = base.axios; var vm = new base.vue({ el: '#app', //掛載點 mixins: [base.mixin], //混入,類似基類的概念 components: { 'blog-post': blogPost //局部註冊組件,註意局部註冊的組件在其子組件中不可用。 }, data: { posts: [ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ], postFontSize: 1 }, //created鉤子函數 created: function () { console.log('This is index created'); }, //mounted鉤子函數 mounted: function () { console.log('This is index mounted'); }, //方法 methods: { //放大文本 onEnlargeText: function (enlargeFontSize) { var _this = this; _this.postFontSize += enlargeFontSize } } }); });
其中 require_config.js 代碼如下:
//主要用來配置模塊的載入位置(設置短模塊名) require.config({ baseUrl: '/js/lib', //設置根目錄 paths: { //如果沒有設置根目錄則需要填寫完整路徑 'vue': 'vue', 'axios': 'axios', 'jquery': 'jquery-3.6.3', //paths還有一個重要的功能,就是可以配置多個路徑,如果遠程cdn庫沒有載入成功,可以載入本地的庫,如下: //'jquery': ['http://libs.baidu.com/jquery/2.0.3/jquery', '/js/lib/jquery-3.6.3'], } });
其中 base.js 代碼如下:
//define用來自定義模塊 //第一個參數:載入依賴模塊,可以是require_config中定義的短模塊名,也可以是完整的模塊路徑(去掉.js尾碼名) //第二個參數:執行載入完後的回調函數 define(['vue', 'axios', '../components/buttonCounter'], function (vue, axios, buttonCounter) { //TODO 此處可以處理一些公共的邏輯 //vue.component('component-a', { /* ... */ }); //全局註冊組件 //vue.mixin({...}); //全局混入 /* 定義組件名的方式有兩種: 1、使用 kebab-case (短橫線分隔命名) 當使用 kebab-case (短橫線分隔命名) 定義一個組件時,你也必須在引用這個自定義元素時使用 kebab-case,例如 <my-component-name> 2、使用 PascalCase (首字母大寫命名) 當使用 PascalCase (首字母大寫命名) 定義一個組件時,你在引用這個自定義元素時兩種命名法都可以使用。 也就是說 <my-component-name> 和 <MyComponentName> 都是可接受的。 註意,儘管如此,直接在 DOM (即非字元串的模板) 中使用時只有 kebab-case 是有效的。 */ //Vue.component(...) 的第一個參數為組件名。 vue.component('button-counter', buttonCounter); //全局註冊 return { vue: vue, axios: axios, //Vue混入 mixin: { //數據 data: function () { return { domain: '', //功能變數名稱 } }, //組件 components: { }, //created鉤子函數 created: function () { console.log('This is base created'); }, //mounted鉤子函數 mounted: function () { console.log('This is base mounted'); }, //方法 methods: { //測試 doTest: function () { console.log('This is base doTest'); }, //獲取功能變數名稱 getDomain: function () { var _this = this; _this.domain = 'https://www.baidu.com'; }, } }, }; });
運行結果如下:
三、組件註冊
1、組件名大小寫
定義組件名的方式有兩種:
1)使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
當使用 kebab-case (短橫線分隔命名) 定義一個組件時,你也必須在引用這個自定義元素時使用 kebab-case,例如 <my-component-name>
。
2)使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
當使用 PascalCase (首字母大寫命名) 定義一個組件時,你在引用這個自定義元素時兩種命名法都可以使用。也就是說 <my-component-name>
和 <MyComponentName>
都是可接受的。註意,儘管如此,直接在 DOM (即非字元串的模板) 中使用時只有 kebab-case 是有效的。
2、全局註冊
到目前為止,我們用過 Vue.component
來創建組件:
Vue.component('my-component-name', { // ... 選項 ... })
這些組件是全局註冊的。也就是說它們在註冊之後可以用在任何新創建的 Vue 根實例 (new Vue
) 的模板中。
3、局部註冊
全局註冊往往是不夠理想的。比如,如果你使用一個像 webpack 這樣的構建系統,全局註冊所有的組件意味著即便你已經不再使用一個組件了,它仍然會被包含在你最終的構建結果中。這造成了用戶下載的 JavaScript 的無謂的增加。
在這些情況下,你可以通過一個普通的 JavaScript 對象來定義組件:
var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ }
然後在 components
選項中定義你想要使用的組件:
new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
對於 components
對象中的每個 property 來說,其 property 名就是自定義元素的名字,其 property 值就是這個組件的選項對象。
註意局部註冊的組件在其子組件中不可用。例如,如果你希望 ComponentA
在 ComponentB
中可用,則你需要這樣寫:
var ComponentA = { /* ... */ } var ComponentB = { components: { 'component-a': ComponentA }, // ... }
四、組件中的Prop
1、Prop 的大小寫 (camelCase vs kebab-case)
HTML 中的 attribute 名是大小寫不敏感的,所以瀏覽器會把所有大寫字元解釋為小寫字元。這意味著當你使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名:
Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' })
<!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>
重申一次,如果你使用字元串模板,那麼這個限制就不存在了。
2、Prop 類型
到這裡,我們只看到了以字元串數組形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
但是,通常你希望每個 prop 都有指定的值類型。這時,你可以以對象形式列出 prop,這些 property 的名稱和值分別是 prop 各自的名稱和類型:
props: { title: String, likes: Number, isPublished: Boolean, commentIds: Array, author: Object, callback: Function, contactsPromise: Promise // or any other constructor }
這不僅為你的組件提供了文檔,還會在它們遇到錯誤的類型時從瀏覽器的 JavaScript 控制台提示用戶。
3、單向數據流
所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外變更父級組件的狀態,從而導致你的應用的數據流向難以理解。
額外的,每次父級組件發生變更時,子組件中所有的 prop 都將會刷新為最新的值。這意味著你不應該在一個子組件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器的控制臺中發出警告。
這裡有兩種常見的試圖變更一個 prop 的情形:
1)這個 prop 用來傳遞一個初始值;這個子組件接下來希望將其作為一個本地的 prop 數據來使用。
在這種情況下,最好定義一個本地的 data property 並將這個 prop 用作其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
2)這個 prop 以一種原始的值傳入且需要進行轉換。
在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
註意在 JavaScript 中對象和數組是通過引用傳入的,所以對於一個數組或對象類型的 prop 來說,在子組件中改變變更這個對象或數組本身將會影響到父組件的狀態。
五、自定義事件
1、事件名
不同於組件和 prop,事件名不存在任何自動化的大小寫轉換。而是觸發的事件名需要完全匹配監聽這個事件所用的名稱。舉個例子,如果觸發一個 camelCase 名字的事件:
this.$emit('myEvent')
則監聽這個名字的 kebab-case 版本是不會有任何效果的:
<!-- 沒有效果 --> <my-component v-on:my-event="doSomething"></my-component>
不同於組件和 prop,事件名不會被用作一個 JavaScript 變數名或 property 名,所以就沒有理由使用 camelCase 或 PascalCase 了。並且 v-on
事件監聽器在 DOM 模板中會被自動轉換為全小寫 (因為 HTML 是大小寫不敏感的),所以 v-on:myEvent
將會變成 v-on:myevent
——導致 myEvent
不可能被監聽到。
因此,我們推薦你始終使用 kebab-case 的事件名。
2、.sync 修飾符
在有些情況下,我們可能需要對一個 prop 進行“雙向綁定”。不幸的是,真正的雙向綁定會帶來維護上的問題,因為子組件可以變更父組件,且在父組件和子組件兩側都沒有明顯的變更來源。
這也是為什麼我們推薦以 update:myPropName
的模式觸發事件取而代之。舉個例子,在一個包含 title
prop 的假設的組件中,我們可以用以下方法表達對其賦新值的意圖:
this.$emit('update:title', newTitle)
然後父組件可以監聽那個事件並根據需要更新一個本地的數據 property。例如:
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
為了方便起見,我們為這種模式提供一個縮寫,即 .sync
修飾符:
<text-document v-bind:title.sync="doc.title"></text-document>
六、插槽
1、具名插槽
有時我們需要多個插槽。例如對於一個帶有如下模板的 <base-layout>
組件:
<div class="container"> <header> <!-- 我們希望把頁頭放這裡 --> </header> <main> <!-- 我們希望把主要內容放這裡 --> </main> <footer> <!-- 我們希望把頁腳放這裡 --> </footer> </div>
對於這樣的情況,<slot>
元素有一個特殊的 attribute:name
。這個 attribute 可以用來定義額外的插槽:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
一個不帶 name
的 <slot>
出口會帶有隱含的名字“default”。
在向具名插槽提供內容的時候,我們可以在一個 <template>
元素上使用 v-slot
指令,並以 v-slot
的參數的形式提供其名稱:
<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout>
現在 <template>
元素中的所有內容都將會被傳入相應的插槽。
最終渲染結果如下所示:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer> </div>
註意 v-slot 只能添加在 <template> 上 (只有一種例外情況),這一點和已經廢棄的 slot attribute 不同。
2、後備內容
有時為一個插槽設置具體的後備 (也就是預設的) 內容是很有用的,它只會在沒有提供內容的時候被渲染。例如在一個 <submit-button>
組件中:
<button type="submit"> <slot></slot> </button>
我們可能希望這個 <button>
內絕大多數情況下都渲染文本“Submit”。為了將“Submit”作為後備內容,我們可以將它放在 <slot>
標簽內:
<button type="submit"> <slot>Submit</slot> </button>
現在當我在一個父級組件中使用 <submit-button>
並且不提供任何插槽內容時:
<submit-button></submit-button>
後備內容“Submit”將會被渲染:
<button type="submit"> Submit </button>
但是如果我們提供內容:
<submit-button>
Save
</submit-button>
則這個提供的內容將會被渲染從而取代後備內容:
<button type="submit"> Save </button>
3、動態插槽名
動態指令參數也可以用在 v-slot 上,來定義動態的插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> </base-layout>
4、具名插槽的縮寫
跟 v-on
和 v-bind
一樣,v-slot
也有縮寫,即把參數之前的所有內容 (v-slot:
) 替換為字元 #
。例如 v-slot:header
可以被重寫為 #header
:
<base-layout> <template #header> <h1>Here might be a page title</h1> </template> <template #default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template #footer> <p>Here's some contact info</p> </template> </base-layout>
七、混入
1、基礎
混入 (mixin) 提供了一種非常靈活的方式,來分發 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!"
2、選項合併
當組件和混入對象含有同名選項時,這些選項將以恰當的方式進行“合併”。
比如,數據對象在內部會進行遞歸合併,併在發生衝突時以組件數據優先。
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('混入對象的鉤子被調用