1. 程式人生 > 其它 >Vue知識點串講

Vue知識點串講

一、Vue知識點串講

複習一下Vue中的核心知識點。

複習完基本的知識點以後,後面再來看一下其它的面試內容

1、基本使用

下面,先來看一段最簡單的程式碼,如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue基本使用</title>
  </head>
  <body>
    <div id="app">
      {{msg}}
    </div>
    <script src="vue.js"></script>
    <script>
      //建立vue例項
      const app = new Vue({
        el: "#app",
        data() {
          return {
            msg: "hello world",
          };
        },
      });
      setTimeout(() => {
        app.msg = "hello Vue";
      }, 1000);
    </script>
  </body>
</html>

在上面的程式碼中,建立了vue的例項,並且指定了資料,最終資料展示在idapp的這個div中,並且在停頓了1秒中以後,通過Vue的例項來修改對應的msg資料。

通過上面的程式碼,我們能夠夠看到Vue的核心理念是資料驅動的理念,所謂的資料驅動的理念:當資料發生變化的時候,使用者介面也會發生相應的變化,開發者並不需要手動的去修改dom.

簡單的理解:就是vue.js幫我們封裝了資料和dom物件操作的對映,我們只需要關心資料的邏輯處理,資料的變

化就能夠自然的通知頁面進行頁面的重新渲染。

這樣做給我們帶來的好處就是,我們不需要在程式碼中去頻繁的操作dom了,這樣提高了開發的效率,同時也避免了在操作Dom

的時候出現的錯誤。

Vue.js的資料驅動是通過MVVM這種框架來實現的,MVVM 框架主要包含三部分:Model,View,ViewMode

Model:指的是資料部分,對應到前端就是JavaScript物件。

View:指的就是檢視部分

ViewModel: 就是連線檢視與資料的中介軟體(中間橋樑)

以上三部分對應到程式碼中的位置如下圖所示:

下面,我們再來看一張圖來理解一下MVVM框架的作用:

資料(Model)和檢視(View)是不能直接通訊的,而是需要通過ViewModel來實現雙方的通訊。當資料(Model)變化的時候,ViewModel能夠監聽到這種變化,並及時通知View檢視做出修改。同樣的,當頁面有事件觸發的時候,ViewModel

也能夠監聽到事件,並通知資料(Model)進行響應。所以ViewModel就相當於一個觀察者,監控著雙方的動作,並及時通知對方進行相應的操作。

簡單的理解就是:MVVM 實現了將業務(資料)與檢視進行分離的功能。

在這裡還需要注意的一點就是:

MVVM框架的三要素:響應式,模板引擎,渲染

響應式:vue如何監聽資料的變化?

模板:Vue的模板如何編寫和解析?怎樣將具體的值替換掉{{msg}}內容,這就是模板引擎的解析。

渲染:Vue如何將模板轉換成html? 其實就是有虛擬DOM 向真實DOM的轉換。

在後面的課程中,我們還會深入探討這塊內容,包括我們自己模擬實現一個數據驅動的框架。

以上內容也是面試的時候,會問到的問題。

2、模板語法

2.1 屬性繫結

屬性的繫結,下面先來看一下關於對屬性的繫結

<div id="app">
      <h2 v-bind:title="msg">
        {{msg}}
      </h2>
    </div>

在上面的程式碼中,我們通過v-bind的方式給h2綁定了一個title屬性。

當然,上面的程式碼我們也可以使用如下的方式來進行簡化

    <div id="app">
      <h2 :title="msg">
        {{msg}}
      </h2>
    </div>

為了避免閃爍的問題,也就是最開始的時候,出現:{{msg}}的情況,可以使用如下的繫結方式。

  <div id="app">
      <h2 :title="msg">
        <!-- {{msg}} -->
        <span v-text="msg"></span>
      </h2>
    </div>

3、 列表渲染

我們可以使用v-for指令基於一個數組來渲染一個列表.v-for指令需要使用item in items形式的語法。其中items 是源陣列,而item則是被迭代的陣列元素的別名。

基本實現的程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <!-- users表示陣列,item表示從陣列中取出的物件,這個名字可以隨意取 -->
        <!-- 注意 v-for必須結合key屬性來使用,它會唯一標識陣列中的每一項,未來當陣列中的那一項改變的時候,它會只更新那一項,好處就是提升效能。注意key的值唯一,不能重複 -->
        <!-- index表示陣列的索引值,該名字可以隨意定義 -->
        <li v-for="(item,index) in users" :key="item.id">
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          users: [
            {
              id: 1,
              name: "張三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },
      });
    </script>
  </body>
</html>

注意:為了能夠保證列表渲染的效能,我們需要給v-for新增key屬性。key值必須唯一,而且不能使用indexrandom作為key的值。

關於這一點是與虛擬DOM演算法密切相關的。在後面的課程中會最為一個重點來探討虛擬DOM的內容。這也是面試的時候經常被問到的問題。

4、v-model

在前面講解vue簡介的時候,我們說過,如果model中的資料發生了改變,會通過ViewModel通知View更新資料,這個效果前面我們也已經演示了,現在演示一下當view中的資料發生了變化後,怎樣通過ViewModel來通知model來完成資料的更新。

其實這就是我們常說的,“雙向資料繫結”

怎樣實現這種效果呢?可以通過v-model來實現。

  <!-- v-model指令用來雙向資料繫結:就是model和view中的值進行同步變化 -->
  <!-- v-model只能在input/textarea/selet  也就是表單元素-->

具體程式碼實現如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>雙向資料繫結</title>
    <script src="./vue.js"></script>
  </head>

  <body>
    <div id="app">
      <input type="text" v-model="userName" />
    </div>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          userName: "zhangsan",
        },
      });
    </script>
  </body>
</html>

怎樣驗證v-model實現了雙向資料繫結呢?

可以開啟控制檯,然後輸入:vm.userName 發現輸出的值為"zhangsan", 取的是模型中的資料。

當在文字框中輸入新的值後,在敲一下vm.userName發現對應的資料發生了變化,也就是檢視中的資料發生了變化,模型中的資料也 會發生變化。

那麼在控制檯中直接給vm.userName="lisi",發現文字框中的值也發生了變化。

關於v-model 這個知識點,面試的時候經常會被問到的一個問題就是,自己能否模擬實現一個類似於v-model的雙向資料繫結的效果。關於這個問題你可以先思考一下,在後面的課程中,我們會詳細的講解。

5、v-on

怎樣監聽dom的事件呢?可以通過v-on指令完成,具體的程式碼如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <span>{{name}}</span>
        <!-- 通過v-on來指定對應的事件,然後後面跟上對應的方法名,方法的定義在methods完成 -->
        <button v-on:click="changeName">更換姓名</button>
    </div>
    <script>
        var vm = new new Vue({
            el: '#app',
            data: {
                name: 'zhangsan'
            },
            // 通過methods完成函式或方法的定義
            methods: {
                changeName() {
                    // 在methods中要獲取data中的屬性,需要通過this來完成,this表示的是vue例項。
                    this.name = "itcast"
                }
            }
        })
    </script>
</body>
</html>

還可以通過簡寫的形式。建議以後都使用簡寫的形式

<button @click="changeName">更換姓名</button>

帶引數的形式如下:

<button @click="changeNameByArg('laowang')">帶引數的情況</button>


<script>
        var vm = new new Vue({
            el: '#app',
            data: {
                name: 'zhangsan'
            },
            // 通過methods完成函式或方法的定義
            methods: {
                changeName() {
                    // 在methods中要獲取data中的屬性,需要通過this來完成,this表示的是vue例項。
                    this.name = "itcast"
                },
                changeNameByArg(userName) {
                    this.name = userName
                }
            }
        })
    </script>

除了繫結滑鼠的單擊事件以外,也可以繫結鍵盤的實際。

例如,頁面有有一個文字框,使用者在該文字框中輸入內容,按下回車鍵,獲取到使用者輸入的內容。

<div id="app">
      <span>{{name}}</span>
      <!-- 通過v-on來指定對應的事件,然後後面跟上對應的方法名,方法的定義在methods完成 -->
      <button @click="changeName">更換姓名</button>
      <button @click="changeNameByArg('laowang')">帶引數的情況</button>
    <!--給文字框新增鍵盤事件-->
      <input type="text" @keydown.enter="changeUserName" v-model="name" />
    </div>

mehtods中定義changeUserName方法

 // 通過methods完成函式或方法的定義
        methods: {
          changeName() {
            // 在methods中要獲取data中的屬性,需要通過this來完成,this表示的是vue例項。
            this.name = "itcast";
          },
          changeNameByArg(userName) {
            this.name = userName;
          },
             //定義處理文字框鍵盤事件的方法。
          changeUserName() {
            console.log(this.name);
          },
        },

在上面的案例中,我們使用了按鍵的修飾符:.enter,在官方文件中,還有其它的按鍵修飾符,如下所示:

https://cn.vuejs.org/v2/guide/events.html#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6

與之相關的就是事件修飾符,如下所示:

https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6

以上內容,大家可以在課下的時候,仔細看一下。

6、Class與Style繫結

這塊主要內容主要與樣式設定有關。

操作元素的 class 列表和內聯樣式是資料繫結的一個常見需求。因為它們都是 attribute,所以我們可以用 v-bind 處理它們:只需要通過表示式計算出字串結果即可。不過,字串拼接麻煩且易錯。因此,在將 v-bind 用於 classstyle 時,Vue.js 做了專門的增強。表示式結果的型別除了字串之外,還可以是物件或陣列。

下面先來看一下Class的繫結。

在"列表渲染"中給每個列表項新增對應的樣式。

   <style>
      .actived {
        background-color: #dddddd;
      }
    </style>

下面給li列表新增上面所定義的樣式。

  <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:true}"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>

在上面的程式碼中,我們可以看到,給li標籤綁定了class屬性,同時actived的值為true,表示給li新增actived樣式。

現在有一個需求,就是當滑鼠移動到列表項上的時候,更改對應的背景色。

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >

在對class進行繫結的時候,做了一個判斷,判斷一下selectItem是否與item相等,如果相等新增樣式。

當滑鼠移動到某個li 列表上的時候,觸發mousemove事件,將item的值給selectItem.

data中定義selectItem.

如下所示:

  data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "張三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },

完整 程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <ul>
        <!-- users表示陣列,item表示從陣列中取出的物件,這個名字可以隨意取 -->
        <!-- 注意 v-for必須結合key屬性來使用,它會唯一標識陣列中的每一項,未來當陣列中的那一項改變的時候,它會只更新那一項,好處就是提升效能。注意key的值唯一,不能重複 -->
        <!-- index表示陣列的索引值,該名字可以隨意定義 -->
        <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "張三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },
      });
    </script>
  </body>
</html>

下面,我們再來看一下Style的繫結。


        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>

通過上面的程式碼,可以看到通過繫結style的方式來處理樣式是非常麻煩的。

7、條件渲染

v-if和v-show指令可以用來控制元素的顯示和隱藏

下面,我們先來看一下v-if的應用。

這裡還是對使用者資料進行判斷。

    <div id="app">
      <p v-if="users.length===0">沒有任何使用者資料</p>

      <ul v-else>
        <!-- users表示陣列,item表示從陣列中取出的物件,這個名字可以隨意取 -->
        <!-- 注意 v-for必須結合key屬性來使用,它會唯一標識陣列中的每一項,未來當陣列中的那一項改變的時候,它會只更新那一項,好處就是提升效能。注意key的值唯一,不能重複 -->
        <!-- index表示陣列的索引值,該名字可以隨意定義 -->
        <!-- <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li> -->

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
    </div>

在上面的程式碼中,我們首先對users陣列做了一個判斷,如果沒有資料,就在頁面上展示:“沒有任何使用者資料”

否則渲染整個列表。

上面是關於v-if的使用,下面看一下v-show.

v-show 是通過css屬性display控制元素顯示,元素總是存在的。

v-if:通過控制dom來控制元素的顯示和隱藏,如果一開始條件為false,元素是不存在的。

什麼時候使用v-show,什麼時候使用v-if呢?

如果需要頻繁的控制元素的顯示與隱藏,建議使用v-show. 從而避免大量DOM操作,提高效能。

而如果某個元素滿足條件後,渲染到頁面中,並且以後變化比較少,可以使用v-if

8、計算屬性

計算屬性出現的目的是解決模板中放入過多的邏輯會讓模板過重且難以維護的問題.

計算屬性是根據data中已有的屬性,計算得到一個新的屬性.

下面,我們可以通過一個案例來學習一下計算屬性、

在一個文字框中輸入第一個名字,第二個文字框中輸入第二個名字,然後展示全部名稱。

<body>
    <div id="app">
        <input type="text" v-model="firstName">
        <input type="text" v-model="lastName">
        <!-- 這樣是模板邏輯變得非常複雜,不易維護 -->
        <div>全名:{{firstName + lastName}}</div>


        <div>全名:{{fullName}}</div>
    </div>
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName: '',
                lastName: ''
            },
            // 建立計算屬性通過computed關鍵字,它是一個物件
            computed: {
                // 這裡fullName就是一個計算屬性,它是一個函式,但這個函式可以當成屬性來使用
                fullName() {
                    return this.firstName + this.lastName
                }
            }
        })
    </script>
</body>

瞭解了計算屬性後,下面對使用者列表新增一個功能,要求是計算總人數。

可以在ul列表下面,新增如下的程式碼。

      <p>
        總人數:{{users.length+"個"}}
      </p>

最終展示出了,對應的人數,但是這裡在模板中做了運算(在這裡做了字串拼接,雖然計算簡單,但是最好還是通過計算屬性來完成),為了防止在模板中放入過多的邏輯計算,這裡可以使用計算屬性來解決。

下面對程式碼進行改造:

  <p>
        <!-- 總人數:{{users.length+"個"}} -->
        總人數:{{total}}
      </p>

計算屬性實現:

  <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          users: [
            {
              id: 1,
              name: "張三",
            },
            {
              id: 2,
              name: "李四",
            },
            {
              id: 3,
              name: "老王",
            },
          ],
        },

        computed: {
          total() {
            // 計算屬性是有快取性:如果值沒有發生變化,則頁面不會重新渲染
            return this.users.length + "個";
          },
        },
      });
    </script>v

通過上面的程式碼,可以看到使用計算屬性,讓介面變得更加的簡潔。

使用計算屬性還有一個好處:

其實細心的話就會發現,呼叫methods裡的方法也能實現和計算屬性一樣的效果,既然使用methods就可以實現,那為什麼還需要計算屬性呢?原因就是計算屬性是基於他的依賴快取的(所依賴的還是data中的資料)。一個計算屬性所依賴的資料發生變化時,他才會重新取值

也就是說:只要相關依賴沒有改變,對此訪問計算屬性得到的是之前緩 存的結果,不會多次執行。

下面我們測試一下:

 <p>
        <!-- 總人數:{{users.length+"個"}} -->
        總人數:{{total}} 總人數:{{total}}
      </p>

在上面的程式碼中,我們使用total了兩次。

下面在看一下關於計算屬性中的程式碼修改:

 computed: {
          total() {
            console.log("aaa");
            // 計算屬性是有快取性:如果值沒有發生變化,則頁面不會重新渲染
            return this.users.length + "個";
          },
        },

這裡,我們通過console輸出字串aaa,但是在控制檯上只是輸出了一次,因為,第二次使用total的時候,發現值沒有變化,所以直接從快取中獲取了對應的值。並沒有重新進行計算,這樣帶來的好處就是,效能得到了提升。

下面,我們換成methods函式的形式來看一下:

 <p>
        <!-- 總人數:{{users.length+"個"}} -->
        總人數:{{total}} 總人數:{{total}} 總人數:{{getTotal()}}
        總人數:{{getTotal()}}
      </p>

在上面的程式碼中,呼叫了兩次getTotal方法。

getTotal方法的實現如下:

  methods: {
          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
        },

實現的方式是差不多的,但是這裡卻執行了兩次。(注意:由於本案例中給每一個li標籤添加了 *@mousemove*,所以只要滑鼠移動到列表上,就會導致頁面重新渲染,這時會不斷的呼叫getTotal方法。)

所以通過上面案例的演示,可以明確的看出計算屬性是有快取的,也就是所依賴的data屬性中的資料沒有變化,那麼是不會重新計算的。所以提升了對應的效能。

所以說,在進行大量耗時計算的時候,建議使用計算屬性來完成。

如下程式碼:

 data: {
          selectItem: "",
          num: 100
          }

data中定義了num 屬性,並且初始值為100、

下面在計算屬性中進行求和的運算,程式碼實現如下:

 computed: {
          total() {
            console.log("aaa");
            // 計算屬性是有快取性:如果值沒有發生變化,則頁面不會重新渲染
            // return this.users.length + "個";
            let count = 0;
            for (let i = 0; i <= this.num; i++) {
              count += i;
            }
            return count;
          },
        },

通過演示,可以發現計算屬性只是在第一次呼叫的時候,執行了一次,後續由於所依賴的資料num沒有發生變化,所以即時呼叫多次,也並沒有重新進行計算,而是獲取上次計算的結果,所以說在進行大量耗時計算的時候,通過計算屬性可以提升效能。

9、偵聽器

偵聽器就是偵聽data中的資料變化,如果資料一旦發生變化就通知偵聽器所繫結方法,來執行相應的操作。從這一點上,與計算屬性是非常類似的。

但是,偵聽器也有自己獨有的應用場景。

執行非同步或開銷較大的操作。

下面,先來看一下偵聽器的基本使用

我們使用偵聽器來統計總人數。

 <p>
   
        總人數:{{totalCount}}
      </p>

data中定義totalCount屬性。

 data: {
          selectItem: "",
          num: 100,
          totalCount: 0
       }   

使用watch來監聽users陣列的資料變化。

   watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        }

users陣列發生了變化後,就會執行handler這個函式,同時用於加上了immediate屬性,並且該屬性的值為true,表示的就是在初始化繫結的時候,也會去執行偵聽器。因為watch在初始化繫結的時候是不會執行的,等到所監聽的內容改變之後才會去偵聽執行。

以上就是watch偵聽器的基本使用,但是通過這個案例,我們發現還是使用計算屬性來統計總人數更加的方便一些。

當然,偵聽器有自己的應用場景,它的應用場景就是在執行非同步請求或者進行開銷比較大的操作的時候,會使用偵聽器。

下面我們在通過一個案例,來體會一下watch偵聽器的應用場景。

下面我們來看一個非同步操作的情況。就是當用戶在一個文字框中輸入了使用者名稱以後,要將輸入的使用者名稱傳送到服務端,來檢查該使用者名稱是否已經被佔用。

具體的實現程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>偵聽器</title>
  </head>
  <body>
    <div id="app">
      <div>
        <span>使用者名稱</span>
          <!--這裡使用了lazy,保證當文字框失去焦點後,才去執行對應操作-->
        <span><input type="text" v-model.lazy="uname" /></span>
        <span>{{message}}</span>
      </div>
    </div>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          uname: "",
          message: "",
        },
        methods: {
          checkUserName: function (userName) {
            let that = this;
            setTimeout(function () {
              if (userName === "admin") {
                that.message = "使用者名稱已經存在,請更改....";
              } else {
                that.message = "該使用者名稱可以使用.....";
              }
            }, 3000);
          },
        },
        watch: {
          uname: function (value) {
            //呼叫後臺介面,來驗證使用者名稱是被佔用
            this.checkUserName(value);
            this.message = "正在校驗使用者名稱....";
          },
        },
      });
    </script>
  </body>
</html>

以上的案例,就是通過watch來監聽uname的值是否發生變化,如果發生了變化,就通過傳送非同步請求來檢查uname中的值,是否已經被佔用。

通過以上的案例:我們可以看到watch是允許非同步操作的,並且在我們得到最終的結果前,可以設定中間狀態,這些都是計算屬性無法做到的。

最後我們把計算屬性與偵聽器做一個總結,看一下它們的應用場景。

第一點:語境上的差異:

watch適合一個值發生了變化,對應的要做一些其它的事情,適合一個值影響多個值的情形。

例如,上面案例中的使用者名稱檢測,這裡是一個uname發生了變化,但是這裡做了很多其它的事情,例如修改message的值,傳送非同步請求。

而計算屬性computed:一個值由其它的值得來,其它值發生了變化,對應的值也會變化,適合做多個值影響一個值的情形。

例如如下程式碼:

computed:{
    fullName(){
        return this.firstName+' '+this.lastName
    }
}

第二點:計算屬性有快取性。

由於這個特點,我們在實際的應用中,能用計算屬性的,會首先考慮先使用計算屬性。

第三點:偵聽器選項提供了更加通用的方法,適合執行非同步操作或者較大開銷操作。

10、生命週期簡介

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

下面,我們來看一下這些鉤子函式的應用。

通過一個非同步獲取列表資料的案例,來檢視這些生命週期的鉤子函式應用。

在這裡是通過非同步的方式獲取使用者列表的資料。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <p v-if="users.length===0">沒有任何使用者資料</p>

      <ul v-else>
        <!-- users表示陣列,item表示從陣列中取出的物件,這個名字可以隨意取 -->
        <!-- 注意 v-for必須結合key屬性來使用,它會唯一標識陣列中的每一項,未來當陣列中的那一項改變的時候,它會只更新那一項,好處就是提升效能。注意key的值唯一,不能重複 -->
        <!-- index表示陣列的索引值,該名字可以隨意定義 -->
        <!-- <li
          v-for="(item,index) in users"
          :key="item.id"
          :class="{actived:selectItem===item}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li> -->

        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---索引:{{index}}
        </li>
      </ul>
      <p>
        <!-- 總人數:{{users.length+"個"}} -->
        <!-- 總人數:{{total}} 總人數:{{total}} 總人數:{{getTotal()}}
        總人數:{{getTotal()}} -->

        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          num: 100,
          totalCount: 0,
            //指定users預設資料為一個空陣列。
          users: [],
        },
        //元件例項已建立時,執行created方法,來呼叫getUserList方法,傳送非同步請求獲取資料
        //將獲取到的資料交個users這個狀態陣列。
        async created() {
          const users = await this.getUserList();
          this.users = users;
        },
        methods: {
          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
            //在getUserList方法中,模擬一個非同步請求。
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
        // computed: {
        //   total() {
        //     console.log("aaa");
        //     // 計算屬性是有快取性:如果值沒有發生變化,則頁面不會重新渲染
        //     // return this.users.length + "個";
        //     let count = 0;
        //     for (let i = 0; i <= this.num; i++) {
        //       count += i;
        //     }
        //     return count;
        //   },
        // },
      });
    </script>
  </body>
</html>

上面的程式碼,還是對原有的“列表渲染”內容進行更改。

第一:將users的值定義為空陣列

第二:定義getUserList方法,在該方法中模擬非同步操作,最終返回的是一個Promise物件。

第三:在created階段呼叫getUserList方法來獲取資料,將獲取到的資料賦值給users這個狀態陣列,注意這裡需要將created修改成asyncawait的形式。同時還要注意created的執行時機:元件例項已建立時,執行created方法。

現在已經對生命週期有了一個簡單的瞭解,下面我們繼續探討生命週期的內容。

11、生命週期探討

在這一小節中,我們看一下vue生命週期中其它的一些鉤子函式內容。

其實Vue例項的生命週期,主要分為三個階段,分別為

  • 掛載(初始化相關屬性,例如watch屬性,method屬性)
    1. beforeCreate
    2. created
    3. beforeMount
    4. mounted
  • 更新(元素或元件的變更操作)
    1. beforeUpdate
    2. updated
  • 銷燬(銷燬相關屬性)
    1. beforeDestroy
    2. destroyed

下面,我們再來看一道面試題:

關於Vue的生命週期,下列哪項是不正確的?()[單選題]
A、Vue 例項從建立到銷燬的過程,就是生命週期。 
B、頁面首次載入會觸發beforeCreate, created, beforeMount, mounted, beforeUpdate, updated。 
C、created表示完成資料觀測,屬性和方法的運算,初始化事件,$el屬性還沒有顯示出來。 
D、DOM渲染在mounted中就已經完成了。

分析:

選項A是沒有問題的,Vue例項從建立到銷燬的過程就是生命週期。

關於B選項,我們可以通過寫一個程式來進行驗證。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命週期</title>
  </head>
  <body>
    <div id="app">{{foo}}</div>
    <script src="./vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          foo: "foo",
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
       updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
    </script>
    <script></script>
  </body>
</html>

在上面的程式碼中,我們將所有的鉤子函式都新增上了,然後開啟瀏覽器,看下執行結果:

beforCreate
created
beforeMount
mounted

以上就是初次載入時所執行的鉤子函式,並沒有beforeUpdateupdated,所以選項B是錯誤的。

那麼beforeUpdateupdated什麼時候會執行呢?是在,元件或者是元素更新的時候。

下面,我們來測試一下,看一下效果。

首先增加一個"更新"按鈕

  <div id="app">
      {{foo}}
      <button @click="update">更新</button>
    </div>

對應的update方法的實現如下:

  methods: {
          update: function () {
            this.foo = "hello";
          },
        },

update方法中,修改了foo屬性的值。開啟瀏覽器,單擊“更新”按鈕後,看到的效果如下:

beforeUpdate
updated

通過以上的測試,可以驗證在更新元素的時候,會執行在“更新”階段的鉤子函式。

下面,我們在測試一下,看一下“銷燬”階段的鉤子函式的執行。

 <div id="app">
      {{foo}}
      <button @click="update">更新</button>
      <button @click="destroy">銷燬</button>
    </div>

在上面的程式碼中增加了一個銷燬的按鈕,對應的destroy方法的實現如下:

    methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //銷燬資源
            this.$destroy();
          },
        },

destroy方法中,呼叫了系統中的$destroy方法銷燬了所有資源,這時會觸發銷燬階段的鉤子函式,所以這時會輸出

beforeDestroy
destroyed

這時,如果你去單擊“更新”按鈕,就會發現什麼效果也沒有了,也就是無法完成元素的更新了,因為元素已經被銷燬了。

下面,我們通過官方的生命週期圖來再次看一下整個生命週期的流程。也是為了看一下上面所出題的CD的選項是說法否正確。

beforeCreate: Vue例項初始化之後,以及事件初始化,以及元件的父子關係確定後執行該鉤子函式,一般在開發中很少使用

created: 在呼叫該方法之前,初始化會被使用到的狀態,狀態包括props,methods,data,computed,watch.

而且會實現對data中屬性的監聽,也就是在created的時候資料已經和data屬性進行了繫結。(放在data中的屬性當值發生改變的時候,檢視也會改變)。同時也會對傳遞到元件中的資料進行校驗。

所以在執行created的時候,所有的狀態都初始化完成,我們也完全可以在該階段傳送非同步的ajax請求,獲取資料。

但是,在created方法中,是無法獲取到對應的的$el選項,也就是無法獲取Dom. 所以說上題中選項c的說法是正確的。

如下程式碼所示:

        created() {
          console.log("created");
          console.log("el===", this.$el);// undefined
          console.log("data==", this.$data);// 可以獲取資料
          console.log("foo==", this.foo);//可以獲取資料
        },

created方法執行完畢後,下面會判斷物件中有沒有el選項。如果有,繼續執行下面的流程,也就是判斷是否有template選項,如果沒有el選項,則停止整個生命週期的流程,直到執行了vm.$mount(el)

後,才會繼續向下執行生命週期的流程。

下面我們測試一下:

    <script>
      const vm = new Vue({
        // el: "#app",  //去掉了el選項
        data: {
          foo: "fooData",
        },
        methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //銷燬資源
            this.$destroy();
          },
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
          console.log("el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
        updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
    </script>

在上面的程式碼中,我們將el選項去掉了,執行上面的程式碼後,我們發現執行完created方法後,整個流程就停止了。

現在,我們不新增el選項,但是手動執行vm.$mount(el),也能夠使暫停的生命週期進行下去。

如下程式碼所示:

 <script>
      const vm = new Vue({
        // el: "#app",//去掉了el選項
        data: {
          foo: "fooData",
        },
        methods: {
          update: function () {
            this.foo = "hello";
          },
          destroy: function () {
            //銷燬資源
            this.$destroy();
          },
        },
        beforeCreate() {
          console.log("beforCreate");
        },
        created() {
          console.log("created");
          console.log("el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        },
        beforeMount() {
          console.log("beforeMount");
        },
        mounted() {
          console.log("mounted");
        },
        beforeUpdate() {
          console.log("beforeUpdate");
        },
        updated() {
          console.log("updated");
        },
        beforeDestroy() {
          console.log("beforeDestroy");
        },
        destroyed() {
          console.log("destroyed");
        },
      });
      vm.$mount("#app");//添加了$mount方法
    </script>

執行上面的程式碼,可以看到,雖然vm物件中沒有el引數,但是通過$mount(el)動態新增的方式,也能夠使生命週期順利進行。

我們繼續向下看,就是判斷在物件中是否有template選項。

第一:如果Vue例項物件中有template引數選項,則將其作為模板編譯成render函式,來完成渲染。

第二:如果沒有template引數選項,則將外部的HTML作為模板編譯(template),也就是說,template引數選項的優先順序要比外部的HTML

第三:如果第一條,第二條件都不具備,則報錯

下面,我們看一下新增template的情況。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命週期2</title>
  </head>
  <body>
    <script src="./vue.js"></script>
    <div id="app"></div>
    <script>
      const vm = new Vue({
        el: "#app",
        template: "<p>Hello {{message}}</p>",
        data: {
          message: "vue",
        },
      });
    </script>
  </body>
</html>

以上是在Vue例項中新增template的情況。

那麼這裡有一個比較有趣的問題就是,當模板同時放在template引數選項和外部HTML中,會出現什麼情況呢?

如下程式碼所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>生命週期2</title>
  </head>
  <body>
    <script src="./vue.js"></script>
    <div id="app">
      <p>你好</p>
    </div>
    <script>
      const vm = new Vue({
        el: "#app",
        template: "<p>Hello {{message}}</p>",
        data: {
          message: "vue",
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們添加了template屬性,同時也在外部添加了模板內容,但是最終在頁面上顯示的是Hello vue 而不是“你好”。就是因為template引數的優先順序比外部HTML的優先順序要高。

當然,我們在開發中,基本上都是使用外部的HTML模板形式,因為更加的靈活。

在這裡,還需要你再次思考一個問題,就是為什麼先判斷 el 選項,然後在判斷template選項呢?

其實通過上面的總結,我們是可以完全總結出來的。

就是因為Vue需要通過el的“選擇器”找到對應的template.也就是說,Vue首先通過el引數去查詢對應的template.如果沒有找到template引數,則到外部HTML中查詢,找到後將模板編譯成render

函式(Vue的編譯實際上就是指Vue把模板編譯成render函式的過程)。

下面,我們繼續看一下生命週期的流程圖。

接下來會觸發beforeMount這個鉤子函式:

在執行該鉤子函式的時候,虛擬DOM已經建立完成,馬上就要渲染了,在這裡可以更改data中的資料,不會觸發updated, 其實在created中也是可以更改資料,也不會觸發updated函式

測試程式碼如下:

  beforeMount() {
          console.log("beforeMount");
          console.log("beforeMount el===", this.$el);
          console.log("data==", this.$data);
          //this.foo = "abc"; //修改資料
          console.log("foo==", this.foo);
        },

通過上面的程式碼,我們可以獲取el中的內容,同時也可以修改資料。

但是,這裡需要注意的輸入的el中的內容,{{foo}}還沒有被真正的資料替換掉。而且對應的內容還沒有掛載到頁面上。

下面執行了Create VM.$el and replace "el" with it

經過這一步後,在模板中所寫的{{foo}}會被具體的資料所替換掉。

所以下面執行mounted的時候,可以看到真實的資料。同時整個元件內容已經掛載到頁面中了,資料以及真實DOM都已經處理好了,可以在這裡操作真實DOM了,也就是在mounted的時候,頁面已經被渲染完畢了,在這個鉤子函式中,我們可以去傳送ajax請求。

  mounted() {
          console.log("mounted");
          console.log("mounted el===", this.$el);
          console.log("data==", this.$data);
          console.log("foo==", this.foo);
        }

所以說,最開始的問題中,D選項:DOM渲染在mounted中就已經完成了這句話的描述也是正確的。

下面繼續看生命週期的流程,如下圖所示:

當整個元件掛在完成後,有可能會進行資料的修改,當Vue發現data中的資料發生了變化,會觸發對應元件的重新渲染,先後呼叫了beforeUpdateupdated鉤子函式。

updated之前beoreUpdate之後有一個非常重要的操作就是虛擬DOM會重新構建,也就是新構建的虛擬DOM與上一次的虛擬DOM樹利用diff演算法進行對比之後重新渲染。

而到了updated這個方法,就表示資料已經更新完成,dom也重新render完成。

下面如果我們呼叫了vm.$destroy方法後,就會銷燬所有的資源。

首先會執行beforeDestroy 這個鉤子函式,這個鉤子函式在例項銷燬前呼叫,在這一步,例項仍然可用。

在該方法中,可以做一些清理的工作,例如:清除定時器等。

但是執行到destroyed鉤子函式的時候,Vue例項已經被銷燬,所有的事件監聽器會被移除,所有的子例項也會被銷燬。

最後做一個簡單的總結:

beforeCreate( )// 該鉤子函式執行時,元件例項還未建立.
created()//元件初始化完畢,各種資料可以使用,可以使用ajax傳送非同步請求獲取資料
beforeMounted()// 未執行渲染,更新,虛擬DOM完成,真實DOM未建立
mounted()// 初始化階段結束,真實DOM已經建立,可以傳送非同步請求獲取資料,也可以訪問dom元素
beforeUpdate()//更新前,可用於獲取更新前各種狀態資料
updated()//更新後執行該鉤子函式,所有的狀態資料是最新的。
beforeDestroy() // 銷燬前執行,可以用於一些定時器的清除。
destroyed()//元件已經銷燬,事件監聽器被移除,所有的子例項也會被銷燬。

以上為生命週期的內容。

12、元件化應用

12.1 元件概述

在這一小節中,重點要理解的就是元件的程式設計思想。

元件表示頁面中的部分功能(包含自己的邏輯與樣式),可以組合多個元件實現完整的頁面功能。

如下圖所示:

問題是,如何確定頁面中哪些內容劃分到一個元件中呢?

但你如何確定應該將哪些部分劃分到一個元件中呢?你可以將元件當作一種函式或者是物件來考慮(函式的功能是單一的),根據[單一功能原則]來判定元件的範圍。也就是說,一個元件原則上只能負責一個功能。如果它需要負責更多的功能,這時候就應該考慮將它拆分成更小的元件。

當然,在上圖中,我們發現’Name‘和'Price' 表頭 並沒有單獨的劃分到一個元件中,主要考慮的是功能簡單,就是展示的作用,所以沒有劃分到單獨一個元件中。如果,該表頭具有了一些比較複雜的功能,例如排序。那麼這裡可以單獨的將表頭內容劃分到一個元件中。

元件有什麼特點呢?

可複用、維護、可組合

可複用:每個元件都是具有獨立功能的,它可以被使用在多個場景中。

可組合:一個元件可以和其它的元件一起使用或者可以直接巢狀在另一個元件內部。

可維護:每個元件僅僅包含自身的邏輯,更容易被理解和維護。

下面,看一下怎樣建立元件?

12.2 元件的基本使用

元件具體的建立過程如下:

 Vue.component('index', {
            template: '<div>我是首頁的元件</div>'
        })

第一個引數指定了所建立的元件的名字,第二個引數指定了模板。

元件建立好以後,具體的使用方式如下:

<div id="app">
      <index></index>
</div>

注意:1. 模板template中只能有一個根節點;2. 元件的名字,如果採用駝峰命令的話,在使用的時候,就要加上 “-”,比如元件名字叫indexA,那麼在使用的時候就叫index-a。

例如:

  Vue.component('componentA', {
            template: "<div>建立一個新的元件</div>"
        })

元件的使用

   <component-a></component-a>

在Vue例項中所使用的選項,在元件中都可以使用,但是要注意data,在元件中使用時必須是一個函式。

下面建立一個about元件。

  Vue.component('about', {
            template: '<div>{{msg}}<button @click="showMsg">單擊</button></div>',
            data() {
                return {
                    msg: '大家好'
                }
            },
            methods: {
                showMsg() {
                    this.msg = "關於元件"
                }
            }
        })

元件的使用如下:

  <about></about>

在元件中關於data不是一個物件,而是一個函式的原因,官方文件有明確的說明

https://cn.vuejs.org/v2/guide/components.html

元件建立完整的程式碼如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>元件建立</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <component-a></component-a>
        <index></index>
        <index></index>
        <about></about>
    </div>

    <script>
        Vue.component('componentA', {
            template: "<div>建立一個新的元件</div>"
        })
        Vue.component('index', {
            template: '<div>我是首頁的元件</div>'
        })
        Vue.component('about', {
            template: '<div>{{msg}}<button @click="showMsg">單擊</button></div>',
            data() {
                return {
                    msg: '大家好'
                }
            },
            methods: {
                showMsg() {
                    this.msg = "關於元件"
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

</html>

在使用元件的時候,需要注意以下幾點內容:

第一點:data必須是一個函式

關於這一點,官方文件有比較詳細清楚的說明:https://cn.vuejs.org/v2/guide/components.html

第二:元件模板中必須有一個跟元素。

第三:元件模板內容可以使用模板字串。

    Vue.component("about", {
        template: `<div>
                 {{msg}}
                <button @click='showMsg'>單擊
                </button>
            </div>`,
        data() {
          return {
            msg: "大家好",
          };
        },
        methods: {
          showMsg() {
            this.msg = "關於VUE元件";
          },
        },
      });

在上面的程式碼中,我們在元件的模板中使用類模板字串,這樣就可以調整對應的格式,例如換行等。

第四:現在我們建立的元件是全域性元件,可以在其它元件中使用。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>元件基本使用</title>
  </head>
  <body>
    <div id="app">
      <index></index>
      <component-a></component-a>
      <about></about>
      <!-- 使用HelloWorld元件 -->
      <hello-world></hello-world>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("index", {
        template: "<div>我是Index元件</div>",
      });
      //   建立了HelloWorld元件
      Vue.component("HelloWorld", {
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: "<div>{{ msg}}</div>",
      });
      //   使用HelloWorld元件
      Vue.component("componentA", {
        template: "<div>我是一個新的元件:<HelloWorld></HelloWorld></div>",
      });

      Vue.component("about", {
        template: `<div>
                 {{msg}}
                <button @click='showMsg'>單擊
                </button>
            </div>`,
        data() {
          return {
            msg: "大家好",
          };
        },
        methods: {
          showMsg() {
            this.msg = "關於VUE元件";
          },
        },
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們又建立了一個HelloWorld元件,並且在componentA元件中去使用了HelloWorld元件,這裡還需要注意的一點就是,在componentA這個元件中使用HelloWorld這個元件的時候,可以使用駝峰命名的方式,但是在<div id="app"></div>這個普通的標籤模板中,必須使用短橫線的方式,才能使用元件。

12.3 區域性元件註冊

我們可以在一個元件中,再次註冊另外一個元件,這樣就構成了父子關係。

可以通過components 來建立對應的子元件。

元件的建立過程如下:

<script>
        Vue.component('father', {
            template: '<div><p>我是父元件</p><son></son></div>',
            components: {
                // 建立一個子元件
                son: {
                    template: '<p>我是子元件</p>'
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>

元件的使用

   <div id="app">
        <father></father>
    </div>

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>父子元件建立</title>
    <script src="./vue.js"></script>
</head>

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        Vue.component('father', {
            template: '<div><p>我是父元件</p><son></son></div>',
            components: {
                // 建立一個子元件
                son: {
                    template: '<p>我是子元件</p>'
                }
            }
        })
        var vm =  new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>
</html>

在上面的程式碼中,我們是在全域性的father元件中,又建立了一個子元件son.

那麼son這個子元件也就是一個區域性的元件。也就是它只能在father元件中使用。

當然,我們在father中定義子元件son的時候,直接在其內部構件模板內容,這樣如果程式碼非常多的時候,就不是很直觀。

所以這裡,我們可以將son元件,單獨的進行定義,然後在father元件中進行註冊。

改造後的程式碼如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>區域性元件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子元件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      Vue.component("father", {
        template: "<div><p>我是父元件</p><son></son></div>",
        components: {
          // 建立一個子元件
          //   son: {
          //     template: "<p>我是子元件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們將son元件單獨的進行了定義,這時注意寫法,是一個物件的格式,在物件中包含了關於元件很重要的內容為data函式與template屬性。

同時在father元件中通過components屬性完成了對son元件的註冊。

我們說過son元件是一個區域性的元件,那麼只能在其註冊的父元件中使用。

現在,我們可以測試一下:

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>區域性元件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <!-- 使用ComponentA元件 -->
      <component-a></component-a>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子元件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      //定義ComponentA元件
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
      });
      Vue.component("father", {
        template: "<div><p>我是父元件</p><son></son></div>",
        components: {
          // 建立一個子元件
          //   son: {
          //     template: "<p>我是子元件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們又建立了一個全域性的元件ComponentA,並且在該元件中使用了son元件,注意這裡沒有在ComponentA中使用components來註冊son元件,而是直接使用。同時在<div id='app'></div>中使用了ComponentA元件。這時在瀏覽器中,開啟上面的程式,會出現錯誤。

如果現在就想在ComponentA元件中使用son元件,就需要使用components來註冊。

      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });

現在在ComponentA元件中已經註冊了son元件,這時重新整理瀏覽器就不會出錯了。

在上面這些案例中,我們是在一個全域性的元件中註冊一個區域性的元件,其實,我們也可以在Vue例項中,

註冊對應的區域性元件。因為,我們也可以將vue例項作為一個元件。

詳細程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>區域性元件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <component-a></component-a>
      <hello-msg></hello-msg>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子元件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      // 定義HelloMsg元件
      const HelloMsg = {
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });
      Vue.component("father", {
        template: "<div><p>我是父元件</p><son></son></div>",
        components: {
          // 建立一個子元件
          //   son: {
          //     template: "<p>我是子元件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {},
        components: {
          "hello-msg": HelloMsg,
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們又建立了一個元件HelloMsg

然後將HelloMsg元件註冊到了 Vue例項中,注意:在進行註冊的時候的語法格式。

左側為元件的名稱,由於這個元件建立的時候採用的是駝峰命名的方式,所以元件的名稱採用短橫線的方式。

右側為元件的內容。

下面就可以在其<div id="app"></div>中使用了。

同理,在其他的元件中是無法使用HelloMsg元件的。

13、元件通訊

13.1 父元件向子元件傳值

當我們將整個頁面都拆分了不同的元件以後,這樣就會涉及到元件之間的資料傳遞問題。

常見的元件的通訊可以分為三類:

第一類: 父元件向子元件傳遞資料

第二類: 子元件向父元件傳遞資料

第三類:兄弟元件的資料傳遞。

下面,我們先來看一下父元件向子元件傳遞資料的情況

第一:子元件內部通過props接收傳遞過來的值。

Vue.component('menu-item',{
 props:['title'] // props後面跟一個數組,陣列中的內容為字串,這個字串可以當做屬性類使用。
 template:'<div>{{title}}</div>'   
})

第二: 父元件通過屬性將值傳遞給子元件

<menu-item title="向子元件傳遞資料"> </menu-item>
<menu-item :title="title"></menu-item> <!--可以使用動態繫結的方式來傳值-->

下面看一下具體的案例演示:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        // 建立一個父元件
        Vue.component('father', {
            // 2、在使用子元件的地方,通過v-bind指令來給子元件中的props賦值。
            template: '<div><p>我是父元件</p><son :myName="mySonName"></son></div>',
            data() {
                return {
                    mySonName: '小強'
                }
            },
            components: {
                // 建立一個子元件
                // 1.宣告props,它的作用是:用來接收父元件傳遞過來的資料。
                // props可以跟一個數組,數組裡面的內容可以是字串,這個字串可以當屬性來使用。
                son: {
                    props: ['myName'],
                    template: '<p>我是子元件,我的名字叫{{myName}}</p>'
                }
            }
        })
        var vm = new new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

下面我們在看一個例子,這個例子是前面我們寫的關於區域性元件的案例,我們在這個案例的基礎上實現元件的傳值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>區域性元件</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <div id="app">
      <father></father>
      <component-a></component-a>
      <hello-msg title="你好" :pcontent="content"></hello-msg>
    </div>
    <script>
      const son = {
        data() {
          return {
            msg: "Hello 我是子元件",
          };
        },
        template: `<div>{{msg}}</div>`,
      };
      // 定義HelloMsg元件
      const HelloMsg = {
        props: ["title", "pcontent"],
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg+'----------'+title+'-----------'+pcontent}}</div>`,
      };
      Vue.component("ComponentA", {
        template: "<div><son></son></div>",
        components: {
          son: son,
        },
      });
      Vue.component("father", {
        template: "<div><p>我是父元件</p><son></son></div>",
        components: {
          // 建立一個子元件
          //   son: {
          //     template: "<p>我是子元件</p>",
          //   },
          son: son,
        },
      });
      var vm = new Vue({
        el: "#app",
        data: {
          content: "來自父元件中的內容",
        },
        components: {
          "hello-msg": HelloMsg,
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們首先給hello-msg 這個元件傳遞了一個屬性title,該屬性的值是固定的。在對應的HelloMsg元件內容定義props,來接收傳遞過來的title屬性的值。然後在template模板中展示title的值。

接下來,又在vue例項中指定了一個content的屬性,下面要將該屬性的值傳遞給HelloMsg元件。

    <hello-msg title="你好" :pcontent="content"></hello-msg>

這裡需要動態繫結的方式將content的值傳遞到HelloMsg元件。這裡動態繫結的屬性為pcontent,所以在HelloMsg元件內部,需要在props的陣列中新增一個pcontent,最後在template模板中展示出pcontent的內容。

    // 定義HelloMsg元件
      const HelloMsg = {
        props: ["title", "pcontent"],
        data() {
          return {
            msg: "Hello World",
          };
        },
        template: `<div>{{msg+'----------'+title+'-----------'+pcontent}}</div>`,
      };

通過上面的案例,我們可以看到,在子元件中可以使用props來接收父元件中傳遞過來的資料。

但是,props在進行命名的時候,也是有一定的規則的。

如果在props中使用駝峰形式,模板中需要短橫線的形式,如下程式碼案例所示:

Vue.component('menu-item',{
    //在JavaScript中是駝峰形式
    props:['menuTitle'],
    template:'<div>{{menuTitle}}</div>'
})
<!--在html中是短橫線方式--->
    <menu-item menu-title="hello world"></menu-item>

下面看一下具體的程式碼演示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>元件傳值</title>
  </head>
  <body>
    <div id="app">
      <menu-item :menu-title="ptitle"></menu-item>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("menu-item", {
        props: ["menuTitle"],
        template: `<div>來自{{menuTitle}}</div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          ptitle: "父元件中的資料",
        },
      });
    </script>
  </body>
</html>

下面再來看一下props屬性值的型別。

props 可以接收各種型別的值。

如下:

字串(String

數值(Number)

布林值(Boolean)

陣列(Array)

物件(Object)

下面,將上面的型別都演示一下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>props型別</title>
  </head>
  <body>
    <div id="app">
      <menu-item
        :str="str"
        :num="10"
        b="true"
        :marr="arr"
        :obj="obj"
      ></menu-item>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("menu-item", {
        props: ["str", "num", "b", "marr", "obj"],
        template: `<div>
                <div>{{str}}</div>
                <div>{{typeof num}}</div>
                <div>{{typeof b}}</div>
                <div>
                   <ul>
                    <li :key=item.id v-for='item in marr'>{{item.userName}}</li>
                   </ul>
               </div>
               <div>
                姓名: {{obj.name}}
                年齡:{{obj.age}}
                </div>
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          str: "hello",
          arr: [
            { id: 1, userName: "zhangsan" },
            {
              id: 2,
              userName: "lisi",
            },
          ],
          obj: {
            name: "wangwu",
            age: 18,
          },
        },
      });
    </script>
  </body>
</html>


在上面的程式碼中,向menu-item元件中傳遞了各種型別的資料。

注意:

  <menu-item :str="str" :num="10" b="true" :marr="arr"></menu-item>

在上面的程式碼中,:num="10"表示傳遞的是數字,如果寫成num='10' 表示傳遞的是字元,

同理b="true"傳遞的是字元,如果修改成:b=true表示傳遞的是布林型別。

最後還傳遞了陣列型別與物件型別的內容。

13.2 子元件向父元件傳值

第一:子元件通過自定義事件向父元件傳遞資訊。

<button v-on:click='$emit("countSum")'> 計算</button>

第二:父元件監聽子元件的事件

<menu-item v-on:countSum='sum+=1'></menu-item>

具體的實現步驟如下:

1、構建基本的結構

     <div id="app">      
    </div>
var vm = new Vue({
            el: '#app',
            data: {

            }
        })

2、構建相應的父元件。

  Vue.component('father', {
            template: '<div>我的兒子叫{{mySonName}}</div>',
            data() {
                return {
                    mySonName: ''
                }
            }
  }

3、 構建相應的子元件, 並且單擊子元件中的按鈕給父元件傳值。

  Vue.component('father', {
            template: '<div>我的兒子叫{{mySonName}}</div>',
            data() {
                return {
                    mySonName: ''
                }
            },
        components: {
                son: {
                    data() {
                        return {
                            myName: '小強'
                        }
                    },
                    template: '<button @click="emitMyName">我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 子元件傳值給父元件需要用到$emit()方法,這個方法可以傳遞兩個引數,一個是事件名稱,一個是需要傳遞的資料
                            this.$emit('tellMyFatherMyName', this.myName)
                        }
                    }
                }
            }
  }

4、父元件接收子元件傳遞過來的資料。

注意在父元件中引用子元件,同時指定在子元件中定義的事件。

   Vue.component('father', {
            template: '<div>我的兒子叫{{mySonName}}<son @tellMyFatherMyName="getMySonName"></son></div>',
            data() {
                return {
                    mySonName: ''
                }
            },
            methods: {
                getMySonName(data) {
                    this.mySonName = data;
                }
            }
   }

5、元件使用

  <div id="app">
        <father></father>
    </div>

6、完整程式碼如下:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        Vue.component('father', {
            template: '<div>我的兒子叫{{mySonName}}<son @tellMyFatherMyName="getMySonName"></son></div>',
            data() {
                return {
                    mySonName: ''
                }
            },
            methods: {
                getMySonName(data) {
                    this.mySonName = data;
                }
            },
            components: {
                son: {
                    data() {
                        return {
                            myName: '小強'
                        }
                    },
                    template: '<button @click="emitMyName">我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 子元件傳值給父元件需要用到$emit()方法,這個方法可以傳遞兩個引數,一個是事件名稱,一個是需要傳遞的資料
                            this.$emit('tellMyFatherMyName', this.myName)
                        }
                    }
                }
            }

        })
        var vm = new new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

13.3 兄弟元件之間資料傳遞

兄弟元件傳值,通過事件匯流排完成。

1、定義父元件並且在父元件中,完成兩個兄弟元件的建立。

  <script>
        Vue.component('father', {
            template: '<div><son></son><daughter></daughter></div>',
            components: {
                son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>'
                },
                daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告訴哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {

                        }
                    }
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>

2、建立事件匯流排

通過事件匯流排發射一個事件名稱和需要傳遞的資料 。

  // 建立一個空的vue例項,作為事件匯流排
        var eventbus = new Vue()
          daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告訴哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 通過事件匯流排發射一個事件名稱和需要傳遞的資料
                            eventbus.$emit('tellBroMyName', this.myName)
                        }
                    }
                }
        
        

3、通過eventbus的$on()方法去監聽兄弟節點發射過來的事件

 son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>',
                    mounted() {
                        // 通過eventbus的$on()方法去監聽兄弟節點發射過來的事件
                        // $on有兩個引數,一個是事件名稱,一個是函式,該函式的預設值就是傳遞過來的資料
                        eventbus.$on('tellBroMyName', data => {
                            this.mySisterName = data
                        })
                    }
                },

4、元件的使用

 <div id="app">
        <father></father>
    </div>

5、完整的程式碼如下:

<body>
    <div id="app">
        <father></father>
    </div>
    <script>
        // 建立一個空的vue例項,作為事件匯流排
        var eventbus = new Vue()
        Vue.component('father', {
            template: '<div><son></son><daughter></daughter></div>',
            components: {
                son: {
                    data() {
                        return {
                            mySisterName: ''
                        }
                    },
                    template: '<div>我妹妹叫{{mySisterName}}</div>',
                    mounted() {
                        // 通過eventbus的$on()方法去監聽兄弟節點發射過來的事件
                        // $on有兩個引數,一個是事件名稱,一個是函式,該函式的預設值就是傳遞過來的資料
                        eventbus.$on('tellBroMyName', data => {
                            this.mySisterName = data
                        })
                    }
                },
                daughter: {
                    data() {
                        return {
                            myName: '小雪'
                        }
                    },
                    template: '<button @click="emitMyName">告訴哥哥我叫{{myName}}</button>',
                    methods: {
                        emitMyName() {
                            // 通過事件匯流排發射一個事件名稱和需要傳遞的資料
                            eventbus.$emit('tellBroMyName', this.myName)
                        }
                    }
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {

            }
        })
    </script>
</body>

14、元件插槽應用

14.1 插槽基本使用

生活中的插槽

其實我們生活中有很多很多的插槽。比如電腦的USB插槽、插板中的電源插槽等等。每個插槽都有它們之間的價值。比如電腦的USB插槽,可以用來插U盤,連結滑鼠,連結手機、音響等等,通過這些插槽,大大拓展了原有裝置的功能。

元件中的插槽

元件中的插槽,讓使用者可以決定元件內部的一些內容到底展示什麼,也就是,插槽可以實現父元件向子元件傳遞模板內容。具有插槽的元件將會有更加強大的拓展性,

下面看一個實際應用的例子來體會一下插槽的引用場景。

三個頁面中都有導航欄,基本結構都是一樣的:左中右分別有一個東西,只是顯示的內容不同而已。那我們如何來實現這種結構相似但是內容不同呢?
 你一定是想著,直接定義三個元件,然後在模板中分別顯示不同的內容,對不對?恭喜你,你就快要被炒了。
 首先,如果我們封裝成三個元件,顯然不合適,比如每個頁面都有返回,這部分的內容我們就要重複去封裝
 其次,如果我們封裝成一個,還是不合理,因為有些左側是選單欄,有些中間是搜尋框,有些是文字。
那我們該怎麼辦呢?其實很簡單,用元件插槽。

上面最佳的解決辦法是將共性抽取到元件中,將不同暴露給插槽,一旦我們使用了插槽,就相當於預留了空間空間的內容取決於使用者

如下圖所示:

通過上圖,我們可以在父元件中使用子元件,同時由於在子元件中建立插槽slot,也就是相當於預留了空間,這時在父元件中使用子元件時,可以傳遞不同的內容。

下面看一下插槽的應用

基本使用方式

第一:確定插槽的位置

Vue.component('alert-box',{
 template:`
   <div class="demo-alert-box">
		<strong>子元件</strong>
	    <slot></slot>
   </div>
`
})

在子元件中,通過<slot>確定出插槽的位置。

第二:插槽內容

<alert-box>Hello World</alert-box>

向插槽中傳遞內容。

下面看一下具體的程式碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>插槽基本使用</title>
  </head>
  <body>
    <div id="app">
      <alert-box>程式出現了bug</alert-box>
      <alert-box>程式出現了警告</alert-box>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("alert-box", {
        template: `
                <div>
                    <strong>ERROR:</strong>
                    <slot></slot>
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

通過上面的程式碼我們可以看到,在alert-box這個元件中,定義了一個插槽,也就是預留了一個位置,下面使用該元件的時候,都可以向該插槽中傳遞資料。而<strong>標籤中的內容就相當於是一個公共的內容了。

當然在插槽中也是可以新增預設的內容的。

  <div id="app">
      <alert-box>程式出現了bug</alert-box>
      <alert-box>程式出現了警告</alert-box>
      <alert-box></alert-box>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("alert-box", {
        template: `
                <div>
                    <strong>ERROR:</strong>
                    <slot>預設內容</slot>
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>

在上面的程式碼中,我們給插槽添加了預設的內容,如果在使用alert-box元件的時候,沒有給插槽傳遞值,就會展示插槽中的預設內容。

14.2 具名插槽

所謂的具名插槽就是有名字的插槽。

第一:插槽定義

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

第二:插槽內容

<base-layout>
  <h1 slot="header"> 標題內容</h1>
  <p>
    主要內容    
  </p>  
 <p>
    主要內容    
  </p> 
    <p slot="footer">
        底部內容
    </p>
</base-layout>

下面我們來看一下具體的程式碼實現

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>具名插槽</title>
  </head>
  <body>
    <div id="app">
      <base-layout>
        <p slot="header">頭部內容</p>
        <p>主要內容1</p>
        <p>主要內容2</p>
        <p slot="footer">底部資訊</p>
      </base-layout>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("base-layout", {
        template: `
                <div>
                    <header>
                        <slot name="header"></slot>
                     </header>
                     <main>
                        <slot></slot>
                     </main> 
                     <footer>
                        <slot name="footer"></slot>
                     </footer>   
                     
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中, <p slot="header">頭部內容</p>會插入到base-layout 元件的header這個插槽中。

<p slot="footer">底部資訊</p>會插入到footer這個插槽中。

剩餘的內容會插入到預設的(沒有名稱)的插槽內。

在上面的應用中,有一個問題就是,我們把插槽的名稱給了某個html標籤,例如p標籤,這樣就只能將該標籤插入到插槽中。

但是,在實際的應用中,有可能需要向插槽中插入大量的內容,這時就需要用到template標籤。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>具名插槽</title>
  </head>
  <body>
    <div id="app">
      <!-- <base-layout>
        <p slot="header">頭部內容</p>
        <p>主要內容1</p>
        <p>主要內容2</p>
        <p slot="footer">底部資訊</p>
      </base-layout> -->
      <base-layout>
        <template slot="header">
          <div>標題名稱</div>
          <div>標題區域的佈局</div>
        </template>
        <div>
          中間內容區域的佈局實現
        </div>
        <template slot="footer">
          <div>底部資訊</div>
          <div>對底部內容區域進行佈局</div>
        </template>
      </base-layout>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("base-layout", {
        template: `
                <div>
                    <header>
                        <slot name="header"></slot>
                     </header>
                     <main>
                        <slot></slot>
                     </main> 
                     <footer>
                        <slot name="footer"></slot>
                     </footer>   
                     
                </div>    
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們給template標籤添加了插槽的名稱,並且在template標籤中嵌入了其它的多個標籤,從而完成佈局。

在這裡,可以統一檢視瀏覽器端所生成的程式碼結構。

14.3 作用域插槽

應用場景:父元件對子元件的內容進行加工處理。這也是作用域插槽的一個很重要特性,

下面我們通過一個例子來體會一下這句話的作用。

首先,我們先建立一個使用者列表。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽</title>
  </head>
  <body>
    <div id="app">
      <user-list :list="userList"></user-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("user-list", {
        props: ["list"],
        template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>{{item.userName}}</li>
                 </ul>   
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          userList: [
            {
              id: 1,
              userName: "張三",
            },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們首先建立了一個user-list元件,在這個元件中接收父元件傳遞過來的使用者資料,通過迴圈的方式展示傳遞過來的使用者資料。

現在,這裡有一個新的需求,就是修改某個使用者名稱的顏色,讓其高亮顯示。這個需求應該怎樣來處理呢?

我們是否可以在子元件user-list中實現這個功能呢?

雖然可以,但是一般不建議你這麼做,因為一個元件建立好以後,一般不建議修改。你可以想一下,如果這個元件是其它人建立的,而且很多人都在用,如果直接修改這個子元件,就會造成很多的問題。

所以這裡,還是從父元件中進行修改。也是通過父元件來決定子元件中的哪個使用者名稱進行高亮顯示。

下面對程式碼進行修改:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽</title>
  </head>
  <body>
    <div id="app">
      <user-list :list="userList">
        <template slot-scope="slotProps">
          <strong v-if="slotProps.info.id===2"
            >{{slotProps.info.userName}}</strong
          >
          <span v-else>{{slotProps.info.userName}}</span>
        </template>
      </user-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("user-list", {
        props: ["list"],
        template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>
                        <slot :info="item">
                            {{item.userName}}
                            </slot>
                        </li>
                 </ul>   
                </div>`,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          userList: [
            {
              id: 1,
              userName: "張三",
            },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

通過上面的程式碼可以看到,為了能夠實現父元件決定子元件中哪個使用者名稱能夠高亮顯示,需要在設計子元件的時候,為其新增對應的插槽。

template: `<div>
                <ul>
                    <li :key="item.id" v-for='item in list'>
                        <slot :info="item">
                            {{item.userName}}
                            </slot>
                        </li>
                 </ul>   
                </div>`,

在子元件的template模板中,添加了插槽,同時為其動態繫結一個屬性info(這個屬性的名字是可以隨意命名的),該屬性的值為使用者的資訊。

繫結該屬性的目的就是為了能夠在父元件中獲取使用者的資訊。

下面看一下父元件中的修改

 <div id="app">
      <user-list :list="userList">
        <template slot-scope="slotProps">
          <strong v-if="slotProps.info.id===2"
            >{{slotProps.info.userName}}</strong
          >
          <span v-else>{{slotProps.info.userName}}</span>
        </template>
      </user-list>
    </div>

父元件在使用子元件user-list的時候,這裡為其添加了template這個標籤,而且這個標籤的屬性slot-scope是固定的,為其指定了一個值為slotProps,該值中,儲存的就是從子元件中獲取到的使用者資料。

所以接下來通過slotProps獲取info(注意這裡要與子元件中的slot屬性保持一致)中的使用者資料。然後進行判斷,如果使用者編號為2的,為其加錯,否者正常展示。

通過以上的案例,我們可以看到父元件通過作用域插槽實現了對子元件中資料的處理。其實這也就是為什麼叫做作用域插槽的原因:

是因為模板雖然是在父級作用域(父元件)中渲染的,卻能拿到子元件的資料。

14.4. 作用域插槽案例

下面,我們通過一個列表的案例,來體會一下作用域插槽的應用。

首先我們先來做一個基本的列表元件

這裡,我們首先使用的是具名插槽完成的,如下程式碼所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <my-list>
        <template slot="title">
          使用者列表
        </template>
        <template slot="content">
          <ul>
            <li v-for="item in listData" :key="item.id">{{item.userName}}</li>
          </ul>
        </template>
      </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        template: `
                <div class="list">
                    <div class="list-title">
                        <slot name="title"></slot>
                    </div>
                    <div class="list-content">
                        <slot name="content"></slot>
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "張三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們在子元件my-list中使用了具名插槽。然後父元件在使用子元件my-list的時候,可以通過template標籤加上slot屬性向具名插槽中傳遞資料。

雖然以上的寫法滿足了基本的需求,但是作為元件的使用者,這樣的一個元件會讓我們感覺非常的麻煩,也就是我們在使用my-list這個元件的時候,還需要自己去編寫content區域的迴圈邏輯。這樣就比較麻煩了,下面對上面的程式碼在做一些修改。

為了解決這個問題,我們可以把迴圈寫到子元件中,這樣我們在使用的時候,不需要寫迴圈了,只是傳遞資料就可以了,這樣就方便多了。其實這裡我們就可以不用具名插槽了。

所以修改後的程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <my-list title="使用者列表" :content="listData">
      </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        props: ["title", "content"],
        template: `
                <div class="list">
                    <div class="list-title">

                        
                        {{title}}
                    </div>
                    <div class="list-content">

                        <ul class="list-content">
                            <li v-for="item in content" :key="item.id">{{item.userName}}</li>
                        </ul> 
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "張三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們沒有使用插槽,直接將資料傳遞到子元件my-list中,然後在該子元件中接收到資料,並進行了迴圈遍歷。

經過這一次的改造,滿足了我們前面所提到的易用性問題,但是現在又有了新的問題,元件的拓展性不好。

每次只能生成相同結構的列表,一旦業務需要發生了變化,元件就不再使用了。比如,我現在有了新的需求,在一個列表的每個列表項前面加上一個小的logo,我總不能又寫一個新的元件來適應需求的變化吧?

這裡就可以使用作用域插槽來解決這個問題。

具體的實現程式碼如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>作用域插槽案例</title>
  </head>
  <body>
    <div id="app">
      <!-- 如果沒有傳遞模板,那麼子元件的插槽中只會展示使用者名稱 -->
      <my-list title="使用者列表" :content="listData"></my-list>
        <!-- 傳遞模板 -->
        <my-list title="使用者列表2" :content="listData">
          <template slot-scope="scope">
            <img src="./one.png" width="20"/>
            <span>{{scope.item.userName}}</span>
          </template>
        </my-list>
    </div>
    <script src="./vue.js"></script>
    <script>
      Vue.component("my-list", {
        props: ["title", "content"],
        template: `
                <div class="list">
                    <div class="list-title">
                        {{title}}
                    </div>
                    <div class="list-content">

                   <ul class="list-content">
                            <li v-for="item in content" :key="item.id">
                           <!--這裡將content中的每一項資料繫結到slot的itemb變數上,在父元件中就可以獲取到item變數-->     
                        <slot :item="item">{{item.userName}}</slot>
                                
                                </li>
                        </ul> 
                    </div>
                </div>
            `,
      });
      const vm = new Vue({
        el: "#app",
        data: {
          listData: [
            { id: 1, userName: "張三" },
            {
              id: 2,
              userName: "李四",
            },
            {
              id: 3,
              userName: "王五",
            },
          ],
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們首先在子元件my-list中,添加了作用域的插槽。

 <ul class="list-content">
                            <li v-for="item in content" :key="item.id">
                           <!--這裡將content中的每一項資料繫結到slot的itemb變數上,在父元件中就可以獲取到item變數-->     
                        <slot :item="item">{{item.userName}}</slot>
                                
                                </li>
                        </ul> 

同時在父元件中,使用對應的插槽

 <div id="app">
      <!-- 如果沒有傳遞模板,那麼子元件的插槽中只會展示使用者名稱 -->
      <my-list title="使用者列表" :content="listData"></my-list>
        <!-- 傳遞模板 -->
        <my-list title="使用者列表2" :content="listData">
          <template slot-scope="scope">
            <img src="./one.png" width="20"/>
            <span>{{scope.item.userName}}</span>
          </template>
        </my-list>
    </div>

再回到開始的問題,作用域插槽到底是幹嘛用的?很顯然,它的作用就如官網所說的一樣:將元件的資料暴露出去。而這麼做,給了元件的使用者根據資料定製模板的機會,元件不再是寫死成一種特定的結構。

以上就是作用域插槽的應用,需要你仔細體會。

那麼,在這裡再次問一個問題,就是在你所使用的Vue外掛或者是第三方的庫中,有沒有遇到使用作用域插槽的情況呢?

其實,比較典型的就是element-uitable元件,它就可以通過新增作用域插槽改變渲染的原始資料。

如下圖所示:

14.5 插槽應用總結

為什麼要使用插槽

元件的最大特性就是複用性,而用好插槽能大大提高元件的可複用能力。

元件的複用性常見情形如在有相似功能的模組中,他們具有類似的UI介面,通過使用元件間的通訊機制傳遞資料,從而達到一套程式碼渲染不同資料的效果

然而這種利用元件間通訊的機制只能滿足在結構上相同,渲染資料不同的情形;假設兩個相似的頁面,他們只在某一模組(區域)有不同的UI效果(例如,前面所做的列表,發現可以顯示不同的ui效果),以上辦法就做不到了。可能你會想,使用 v-ifv-else來特殊處理這兩個功能模組,不就解決了?很優秀,解決了,但不完美。極端一點,假設我們有一百個這種頁面,就需要寫一百個v-ifv-else-ifv-else來處理?那元件看起來將不再簡小精緻,維護起來也不容易。

而 插槽 “SLOT”就可以完美解決這個問題

什麼情況下使用插槽

顧名思義,插槽即往卡槽中插入一段功能塊。還是舉剛才的例子。如果有一百個基本相似,只有一個模組功能不同的頁面,而我們只想寫一個元件。可以將不同的那個模組單獨處理成一個卡片,在需要使用的時候將對應的卡片插入到元件中即可實現對應的完整的功能頁。而不是在元件中把所有的情形用if-else羅列出來(這裡還是體會使用者列表的案例)

可能你會想,那我把一個元件分割成一片片的插槽,需要什麼拼接什麼,豈不是隻要一個元件就能完成所有的功能?思路上沒錯,但是需要明白的是,卡片是在父元件上代替子元件實現的功能,使用插槽無疑是在給父元件頁面增加規模,如果全都使用拼裝的方式,和不用元件又有什麼區別(例如,使用者列表案例中需要其他的顯示方式,需要在父元件中進行新增)。因此,插槽並不是用的越多越好

插槽是元件最大化利用的一種手段,而不是替代元件的策略,當然也不能替代元件。如果能在元件中實現的模組,或者只需要使用一次v-else, 或一次v-else-ifv-else就能解決的問題,都建議直接在元件中實現。

15、Vue元件化的理解

關於Vue元件的內容,我們已經學習很多了,那麼你能談一下對Vue元件化的理解嗎?

其實這也是一個比較常見的面試題。

當然,這個問題的面是非常廣的。可以通過以下幾點來描述:

定義:元件是可複用的Vue例項,準確講它是VueComponent的例項,繼承自Vue

優點:元件化可以增加程式碼的複用性,可維護性和可測試性。

使用場景:什麼時候使用元件?以下分類可以作為引數

第一:通用元件:實現最基本的功能,具有通用性,複用性。例如按鈕元件,輸入框元件,佈局元件等。(Element UI元件庫就是屬於這種通用的元件)

第二:業務元件,用於完成具體的業務,具有一定的複用性。例如登入元件,輪播圖元件。

第三:頁面元件,組織應用各部分獨立內容,需要時在不同頁面元件間切換,例如:商品列表頁,詳情頁元件。

如何使用元件

  • 定義:Vue.component()components選項

  • 分類:有狀態元件(有data屬性),functional

  • 通訊:props$emit()/$on()provide/inject

  • 內容分發:<slot><template>v-slot

  • 使用及優化:iskeep-alive非同步元件(這些內容在後面的課程中會詳細的講解)

元件的本質

vue中的元件經歷如下過程 元件配置 => VueComponent例項 => render() => Virtual DOM=> DOM
所以元件的本質是產生虛擬DOM

關於這塊內容,在後面的課程中還會深入的探討,包虛擬dom,以及vue的原始碼。

16、常用API說明

16.1 Vue.set

向響應式物件中新增一個屬性,並確保這個新屬性同樣是響應式的,且會觸發檢視更新。

使用方法:Vue.set(target,propertyName,value)

下面通過一個案例來演示一下,這個案例是在以前所做的使用者列表的案例上進行修改的,

這裡需求是給每個使用者動態的新增身高。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <p v-if="users.length===0">沒有任何使用者資料</p>
      <ul v-else>
        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
        </li>
      </ul>
      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          num: 100,
          totalCount: 0,
          users: [],
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              c.height = 0;
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我首先把列表中,展示的內容做了一個修改,這裡不在顯示索引值,而是展示身高。

   編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}

但是我們知道在users中是沒有height這個屬性的,所以下面可以動態新增這個屬性。

所以在create方法中,呼叫了batchUpdate方法,來動態更新。

    //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },

methods中,添加了batchUpdate方法。

  //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              c.height = 0;
            });
          },

在上面的程式碼中,對users進行遍歷,每遍歷一次,取出一個物件後,動態新增一個屬性height,並且初始值為0.

這樣重新整理瀏覽器,可以看到對應的效果。

下面,我們在做一個功能,就是使用者在一個文字框中,輸入一個身高值,單擊按鈕,統一把所有使用者的身高進行更新。

首先在data中新增一個屬性height,該屬性會與文字框進行繫結。

 data: {
          selectItem: "",
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
        },

下面建立文字框,以及更新按鈕

 <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>

在這裡我們需要在文字框中輸入的值為數字型別,所以添加了一個number的字尾。現在,文字框與height屬性繫結在一起了。下面單擊按鈕後,還是去執行batchUpdate方法。

 //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              c.height = this.height;
            });
          },

這裡我們可以看到,我們是用文字框中輸入的值,更新了users陣列中的height屬性的值。

但是,當我們在瀏覽器中,單擊按鈕進行更新的時候,發現不起作用。

因為,現在動態所新增的height屬性並不是響應式的。

但是,當把滑鼠移動到列表項的時候,資料發生了變化,就是因為這時觸發了我們給列表所新增的mousemove

這個事件,導致頁面重新重新整理,這時發現數據發生變化了。

那麼我們應該怎樣解決這個問題呢?

這就需要,在batchUpdate方法中,使用Vue.set()方法

  batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              Vue.set(c, "height", this.height);
            });
          },

修改的程式碼含義就是通過Vue.set方法,給users陣列中每個物件,設定一個height屬性,這時該屬性就變成了響應式的,同時把 data中的height屬性的值賦值給height.

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <p v-if="users.length===0">沒有任何使用者資料</p>
      <ul v-else>
        <li
          v-for="(item,index) in users"
          :key="item.id"
          :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
          @mousemove="selectItem=item"
        >
          編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
        </li>
      </ul>
      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      new Vue({
        el: "#app",
        data: {
          selectItem: "",
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

16.2 Vue.delete

刪除物件的屬性,如果物件是響應式的,確保刪除能觸發更新檢視。

使用方式:Vue.delete(target,propertyName)

如果使用delete obj['property'] 是不能更新頁面的。

以上兩個方法Vue.set()Vue.delete()等同於以下兩個例項方法。

vm.$set()
vm.$delete()

vm 表示的是Vue的例項。

所以我們在batchUpdate中也可以採用如下的方式,來批量更新使用者的身高資料。

 batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

16.3 vm.$onvm.$emit

16.3.1 列表元件設計

這兩個api在前面的課程中,我們也已經講解過,主要用來實現:事件匯流排。

下面,我們將這兩個API應用到使用者列表這個案例中。主要是把事件匯流排這個應用再次複習一下。

當然,這裡首先是把使用者列表這個案例,按照我們前面所學習的元件的知識,進行拆分一下,實現元件化的應用。

初步改造後的程式碼,如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      // 使用者列表元件建立
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
        <div>
                <p v-if="users.length===0">沒有任何使用者資料</p>
            <ul v-else>
                <li
                v-for="(item,index) in users"
                :key="item.id"
                :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                @mousemove="selectItem=item"
                >
                編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                </li>
            </ul>
      </div>
        `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們首先建立了一個user-list元件,該元件首先會通過props接收傳遞過來的使用者資料。

在這裡我們將props定義成了物件的形式,這樣更容易進行資料型別的校驗,同時還可以設定預設值。

接下來將原來定義在<div id="app"></div> 中的使用者列表,要剪下到user-list元件的template屬性中,同時,我們知道在列表中會用到selectItem屬性,所以在user-listdata中定義該屬性,父元件就不用在定義該屬性了。

下面,我們在<div id="app"></div>中使用該元件,並且傳遞了使用者資料。

  <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

現在使用者列表的元件,在這裡我們就建立好了。

16.3.2 使用者新增元件設計

下面我們在建立一個元件,該元件封裝了一個文字框和新增使用者資訊的按鈕。

程式碼如下:

 //新增使用者元件
      Vue.component("user-add", {
        data() {
          return {
            userInfo: "",
          };
        },
        template: `
            <div>
             <p>
                <input type="text" v-model="userInfo" v-on:keydown.enter="addUser" />
             </p>
             <button @click="addUser">新增使用者</button>
              </div>
            `,
        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            this.$emit("add-user", this.userInfo);
            this.userInfo = "";
          },
        },
      });

在上面的程式碼中,我們建立了user-add 這個元件,該元件最終呈現的就是就是一個文字框與一個新增按鈕。並且通過v-modeluserInfo屬性與文字框進行了繫結。同時,單擊按鈕的時候,執行addUser方法,在該方法中,通過$emit想父元件傳送了一個事件,同時將使用者在文字框中輸入的資料也傳遞過去。

然後清空文字框,

下面看一下父元件的處理。

 <!-- 新增使用者 -->
      <user-add @add-user="addUser"></user-add>

<div id="app"></div> 中使用user-add這個元件,同時接受傳遞過來的事件add-user,然後執行addUser方法。

下面看一下addUser這個方法的具體實現。

vue 例項的methods屬性中,新增addUser這個方法。

//新增使用者的資訊
          addUser(userInfo) {
            this.users.push({
              id: this.users[this.users.length - 1].id + 1,
              name: userInfo,
            });
          },

接受使用者在文字框中輸入的資料,然後新增到users陣列中。

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 新增使用者 -->
      <user-add @add-user="addUser"></user-add>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      //新增使用者元件
      Vue.component("user-add", {
        data() {
          return {
            userInfo: "",
          };
        },
        template: `
            <div>
             <p>
                <input type="text" v-model="userInfo" v-on:keydown.enter="addUser" />
             </p>
             <button @click="addUser">新增使用者</button>
              </div>
            `,
        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            this.$emit("add-user", this.userInfo);
            this.userInfo = "";
          },
        },
      });

      // 使用者列表
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
        <div>
                <p v-if="users.length===0">沒有任何使用者資料</p>
            <ul v-else>
                <li
                v-for="(item,index) in users"
                :key="item.id"
                :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                @mousemove="selectItem=item"
                >
                編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                </li>
            </ul>
      </div>
        `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //新增使用者的資訊
          addUser(userInfo) {
            this.users.push({
              id: this.users[this.users.length - 1].id + 1,
              name: userInfo,
            });
          },

          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

16.3.3 自定義元件實現雙向繫結

在上一個案例中,我們建立了一個user-add這個元件,完成使用者資訊的新增。

並且在該元件的內部,維護了所新增的使用者資訊。

假如,我不想讓user-add這個元件來維護這個使用者資訊,而是讓父元件來維護,應該怎樣處理呢?

 <!-- 新增使用者 -->
      <user-add @add-user="addUser" v-model="userInfo"></user-add>

userInfo的值給v-model.

所以在父元件中要定義userInfo

  new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
        },

下面看一下user-add元件的修改

 Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
            <div>
             <p>
                <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" />
             </p>
             <button @click="addUser">新增使用者</button>
              </div>
            `,
        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
      });

user-add元件中,定義props接收傳遞過來的值,也就是userInfo的值會傳遞給value

下面修改user-add元件中的模板,文字框繫結value值。通過給其新增input事件,在文字框中輸入值後,呼叫onInput方法,在該方法中獲取使用者在文字框中輸入的值,然後傳送input事件。對應的值傳遞給父元件中的userInfo

同時單擊“新增使用者”按鈕的時候,執行addUser方法,在該方法中傳送事件add-user,也不需要傳遞資料了。

同時,父元件中的addUser方法實現如下:

  addUser() {
            this.users.push({
              id: this.users[this.users.length - 1].id + 1,
              name: this.userInfo,
            });
            this.userInfo = "";
          },

直接從data中獲取userInfo的資料。

總結:

以下的寫法

   <user-add @add-user="addUser" v-model="userInfo"></user-add>

等價以下的寫法


<user-add
  v-bind:value="userInfo"
  v-on:input="userInfo = $event"
></user-add>

也就是說v-model就是v-bindv-on的語法糖。

在這裡我們將userInfo的值給了value屬性,而value屬性傳遞到了user-add元件中,所以在user-add元件中要通過props來接收value屬性的值。

user-add元件的文字中,輸入內容後觸發@input 事件,對應的會呼叫onInput方法,在該方法中,執行了

 this.$emit("input", e.target.value);

傳送了input事件,並且傳遞了使用者在文字框中輸入的值。

那很明顯,這時會觸發下面程式碼中的input事件,將傳遞過來的值給userInfo屬性。

<user-add
  v-bind:value="userInfo"
  v-on:input="userInfo = $event"
></user-add>

以上就是v-model的原理,希望仔細體會,這也是面試經常會被問到的問題。

16.3.4. 使用插槽完成內容分發

關於插槽的內容,在前面的的課程中我們已經學習過了,那麼什麼是內容分發呢?

其實就是在使用元件的時候,我們提供具體的資料內容,然後這些內容會插入到元件內部插槽的位置,這就是所謂的內容分發。

下面,要做的事情就是建立一個資訊的提示視窗。例如:當新增使用者成功後,給出相應的提示。

首先先建立樣式:

 <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>

下面建立對應的元件。

   //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
            <slot></slot>
            <span class="message-box-close">關閉</span>
          </div>`,
      });

使用上面的元件

 <div id="app">
      <!-- 彈窗元件 -->
      <message :show="isShow">
        新增使用者成功
      </message>
      <!-- 批量更新身高 -->
      
 </div>     

data 中定義isShow屬性,初始值為false.

 new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          isShow: false,
        },

下面就是當用戶完成新增的時候,彈出該視窗。

 //新增使用者的資訊
          addUser() {
            this.users.push({
              id: this.users[this.users.length - 1].id + 1,
              name: this.userInfo,
            });
            this.userInfo = "";
            //完成使用者新增後,給出相應的提示資訊
            this.isShow = true;
          },

addUser方法中完成了使用者資訊的新增後,將isShow的屬性值設定為true.

這時彈出了對應的視窗。

下面要考慮的就是,單擊視窗右側的“關閉”按鈕,將視窗關閉這個效果應該怎樣實現。

首先給關閉按鈕新增單擊事件。

如下所示:

    //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
            <slot></slot>
            <span class="message-box-close" @click='$emit("close",false)'>關閉</span>
          </div>`,
      });

當單擊關閉按鈕後,會發送一個close事件,同時傳遞的值為false.

下面回到父元件中,對close事件進行處理。

 <!-- 彈窗元件 -->
      <message :show="isShow" @close="closeWindow">
        新增使用者成功
      </message>

close事件觸發後,執行closeWindow方法。

  //關閉視窗
          closeWindow(data) {
            this.isShow = data;
          },

closeWindow方法中,根據子元件傳遞過來的值false,修改isShow屬性的值,這時isShow的值為false.這時視窗關閉。

下面要解決的問題就是,在使用彈窗元件的時候,不僅能傳遞視窗的內容,還能傳遞其它的內容,例如標題等。

那應該怎樣處理呢?

這裡,可以使用具名插槽

程式碼如下:

 <!-- 彈窗元件 -->
      <message :show="isShow" @close="closeWindow">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>

下面修改一下message元件中的內容。

 //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
             <!--具名插槽-->
             <slot name="title">預設標題</slot>
            <slot></slot>
            <span class="message-box-close" @click='$emit("close",false)'>關閉</span>
          </div>`,
      });

在上面定義message元件的時候,指定了具名插槽,名稱為title.要與在父元件中使用message元件的時候指定的名稱保持一致,同時這裡如果沒有傳遞任何內容,將會顯示"預設標題"。

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 彈窗元件 -->
      <message :show="isShow" @close="closeWindow">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 新增使用者 -->
      <user-add @add-user="addUser" v-model="userInfo"></user-add>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
             <!--具名插槽-->
             <slot name="title">預設標題</slot>
            <slot></slot>
            <span class="message-box-close" @click='$emit("close",false)'>關閉</span>
          </div>`,
      });

      //新增使用者元件
      Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
            <div>
             <p>
                <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" />
             </p>
             <button @click="addUser">新增使用者</button>
              </div>
            `,
        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
      });

      // 使用者列表
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
        <div>
                <p v-if="users.length===0">沒有任何使用者資料</p>
            <ul v-else>
                <li
                v-for="(item,index) in users"
                :key="item.id"
                :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                @mousemove="selectItem=item"
                >
                編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                </li>
            </ul>
      </div>
        `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          isShow: false,
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //關閉視窗
          closeWindow(data) {
            this.isShow = data;
          },
          //新增使用者的資訊
          addUser() {
            this.users.push({
              id: this.users[this.users.length - 1].id + 1,
              name: this.userInfo,
            });
            this.userInfo = "";
            //完成使用者新增後,給出相應的提示資訊
            this.isShow = true;
          },

          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

16.3.5 vm.$onvm.$emit應用

現在,關於使用者管理這個案例的一些元件拆分,以及插槽的應用在這我們已經構建好了。

下面就看一下vm.$onvm.$emit的應用。

根據前面的學習,我們知道vm.$onvm.$emit的典型應用就是事件匯流排。

也就是通過在Vue 原型上新增一個Vue例項作為事件匯流排,實現元件間相互通訊,而且不受元件間關係的影響

Vue.prototype.$bus=new Vue()

在所有元件最上面建立事件匯流排,

這樣做的好處就是在任意元件中使用this.$bus訪問到該Vue例項。

下面,我們來看一下事件匯流排的用法。

首先,我們這裡先把事件匯流排創建出來。

    //建立事件匯流排
      Vue.prototype.$bus = new Vue();

下面,在建立一個警告的視窗,也就是當單擊“新增使用者”按鈕的時候,如果使用者沒有填寫使用者名稱給出相應冊錯誤提示。

在這裡先把樣式修改一下:

 <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
      }
      .success {
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .warning {
        background-color: red;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>

然後創建出對應的視窗。


      <!-- 警告 -->
      <message :show="showWarn" @close="closeWindow" class="warning">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>警告</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          請輸入使用者名稱
        </template>
      </message>

注意:在上面的程式碼中,我們使用showWarn這個屬性控制警告視窗的顯示與隱藏。

同時,為其添加了warning樣式,對應的成功的視窗需要新增success 樣式。

同時在data中定義showWarn屬性。

new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          isShow: false,
          showWarn: false, // 控制警告視窗的顯示與隱藏
        },

下面要修改的就是當單擊"新增使用者"按鈕的時候,對addUser方法的修改。

 //新增使用者的資訊
          addUser() {
            if (this.userInfo) {
              this.users.push({
                id: this.users[this.users.length - 1].id + 1,
                name: this.userInfo,
              });
              this.userInfo = "";
              //完成使用者新增後,給出相應的提示資訊
              this.isShow = true;
            } else {
              // 顯示錯誤警告資訊
              this.showWarn = true;
            }
          },

判斷userInfo中是否有值,如果沒有值,展示出錯誤警告資訊。

通過瀏覽器,進行測試。發現如果使用者沒有在文字框中輸入使用者名稱,直接單擊了“新增使用者”,這時給出了錯誤提示的視窗。

但是使用者沒有關閉錯誤提示的視窗,而是直接在文字框中輸入了使用者名稱,然後又點選了"新增使用者"按鈕,這時“成功視窗”與“警告視窗”都顯示出來了。

下面需要解決這個問題。

Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
             <!--具名插槽-->
             <slot name="title">預設標題</slot>
            <slot></slot>
            <span class="message-box-close" @click='$emit("close",false)'>關閉</span>
          </div>`,
        mounted() {
          //給匯流排繫結`message-close`事件
          //也就是監聽是否有`message-close`事件被觸發。
          this.$bus.$on("message-close", () => {
            this.$emit("close", false);
          });
        },
      });

message元件載入完後,給事件匯流排綁定了message-close事件,當該事件觸發後還是向父元件傳送了close事件,這一點與單擊關閉按鈕是一樣的。

下面,怎樣觸發匯流排的message-close事件呢?

我們可以在視窗中新增一個“清空提示欄”按鈕,單擊該按鈕的時候可以觸發message-close事件,從而關閉提示視窗。

  <!-- 清空提示欄 -->
      <div class="toolbar">
        <button @click="$bus.$emit('message-close')">
          清空提示欄
        </button>
      </div>

單擊"清空提示欄"按鈕後,觸發事件匯流排的message-close事件。

最後完善一下closeWindow方法,該方法控制整個提示視窗的關閉

  //關閉視窗
          closeWindow(data) {
            this.isShow = data;
            this.showWarn = data;
          },

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
      }
      .success {
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .warning {
        background-color: red;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 彈窗元件 -->
      <message :show="isShow" @close="closeWindow" class="success">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>

      <!-- 警告 -->
      <message :show="showWarn" @close="closeWindow" class="warning">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>警告</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          請輸入使用者名稱
        </template>
      </message>

      <!-- 清空提示欄 -->
      <div class="toolbar">
        <button @click="$bus.$emit('message-close')">
          清空提示欄
        </button>
      </div>
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 新增使用者 -->
      <user-add @add-user="addUser" v-model="userInfo"></user-add>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      //建立事件匯流排
      Vue.prototype.$bus = new Vue();
      //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        props: ["show"],
        template: `<div class='message-box' v-if="show">
             <!--具名插槽-->
             <slot name="title">預設標題</slot>
            <slot></slot>
            <span class="message-box-close" @click='$emit("close",false)'>關閉</span>
          </div>`,
        mounted() {
          //給匯流排繫結`message-close`事件
          //也就是監聽是否有`message-close`事件被觸發。
          this.$bus.$on("message-close", () => {
            this.$emit("close", false);
          });
        },
      });

      //新增使用者元件
      Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
            <div>
             <p>
                <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" />
             </p>
             <button @click="addUser">新增使用者</button>
              </div>
            `,
        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
      });

      // 使用者列表
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
        <div>
                <p v-if="users.length===0">沒有任何使用者資料</p>
            <ul v-else>
                <li
                v-for="(item,index) in users"
                :key="item.id"
                :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                @mousemove="selectItem=item"
                >
                編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                </li>
            </ul>
      </div>
        `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          isShow: false,
          showWarn: false, // 控制警告視窗的顯示與隱藏
        },
        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //關閉視窗
          closeWindow(data) {
            this.isShow = data;
            this.showWarn = data;
          },
          //新增使用者的資訊
          addUser() {
            if (this.userInfo) {
              if (this.users.length > 0) {
                this.users.push({
                  id: this.users[this.users.length - 1].id + 1,
                  name: this.userInfo,
                });
                this.userInfo = "";
                //完成使用者新增後,給出相應的提示資訊
                this.isShow = true;
              }
            } else {
              // 顯示錯誤警告資訊
              this.showWarn = true;
            }
          },

          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

16.4 vm.$oncevm.$off

關於這兩個方法,大家只需要瞭解一下就可以了。

vm.$once
監聽一個自定義事件,但是隻觸發一次。一旦觸發之後,監聽器就會被移除。

vm.$on('test', function (msg) { console.log(msg) })

vm.$off

移除自定義事件監聽器。

  • 如果沒有提供引數,則移除所有的事件監聽器;

  • 如果只提供了事件,則移除該事件所有的監聽器;

  • 如果同時提供了事件與回撥,則只移除這個回撥的監聽器

vm.$off() // 移除所有的事件監聽器 
vm.$off('test') // 移除該事件所有的監聽器
vm.$off('test', callback) // 只移除這個回撥的監聽器

16.5 refvm.$refs

ref被用來給元素或子元件註冊引用資訊。引用資訊將會註冊在父元件的$refs物件上,如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子元件上,引用就指向元件的例項。

如下程式碼示例,是用來設定輸入框的焦點

<input type="text"  ref="inp" />
mounted(){
    //mounted之後才能訪問到inp
    this.$refs.inp.focus()
}

下面在使用者管理案例中,看一下具體的實現效果。

   //新增使用者元件
      Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
              <div>
               <p>
                  <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" ref="inp" />
               </p>
               <button @click="addUser">新增使用者</button>
                </div>
              `,

        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
        mounted() {
          this.$refs.inp.focus();
        },
      });

在上面的程式碼中,我們首先給user-add元件模板中的文字框添加了ref屬性。

然後,在其所對應的mounted方法中,通過$refs找到文字框,然後為其新增焦點。

回到瀏覽器中,重新整理瀏覽器,可以看到對應的文字框獲取了焦點。

下面,我們在將彈出視窗修改一下:

下面修改一下message模板中的內容。

 //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        // props: ["show"],
        data() {
          return {
            show: false,
          };
        },

        template: `<div class='message-box' v-if="show">
               <!--具名插槽-->
               <slot name="title">預設標題</slot>
              <slot></slot>
              <span class="message-box-close" @click='toggle'>關閉</span>
            </div>`,
        mounted() {
          //給匯流排繫結`message-close`事件
          //也就是監聽是否有`message-close`事件被觸發。
          this.$bus.$on("message-close", () => {
            // this.$emit("close", false);
            this.toggle();
          });
        },
        methods: {
          toggle() {
            this.show = !this.show;
          },
        },
      });

在上面的程式碼中,取消了props,而定義了data屬性,表明的含義就是整個視窗的狀態的控制,也就是提示視窗的顯示與隱藏,都是有自己控制,而不是受外部傳遞的引數來進行控制了。

同時,在該元件中,添加了toggle方法,修改對應的show的狀態。

所以模板中,按鈕的單擊事件觸發以後,呼叫的就是toggle方法,也就是單擊了視窗的右側的關閉按鈕,是通過呼叫toggle方法來完成,視窗的關閉。

同樣事件message-close觸發以後,也是呼叫toggle方法來關閉視窗。

下面看一下關於message模板的使用。

 <!-- 彈窗元件 -->
      <message ref="msgSuccess" class="success">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>

在上面的程式碼中,我們為message元件,添加了ref屬性。

同理表示警告的視窗,也需要新增ref的屬性。

 <!-- 警告 -->
      <message ref="msgWaring" class="warning">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>警告</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          請輸入使用者名稱
        </template>
      </message>

關於data中定義的isShowshowWarn就可以取消了。

data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          // isShow: false,
          // showWarn: false, // 控制警告視窗的顯示與隱藏
        },

當用戶點選“新增使用者”按鈕的時候,執行addUser方法,下面也需要對該方法進行如下修改:

 //新增使用者的資訊
          addUser() {
            if (this.userInfo) {
              if (this.users.length > 0) {
                this.users.push({
                  id: this.users[this.users.length - 1].id + 1,
                  name: this.userInfo,
                });
                this.userInfo = "";
                //完成使用者新增後,給出相應的提示資訊
                // this.isShow = true;
                this.$refs.msgSuccess.toggle();
              }
            } else {
              // 顯示錯誤警告資訊
              // this.showWarn = true;
              this.$refs.msgWaring.toggle();
            }
          },

在上面的程式碼中,我們都是通過$ref 找到對應的視窗,然後呼叫toggle方法,來修改對應的狀態。

因為,我們前面講過如果ref用在子元件上,引用就指向元件的例項.所以可以呼叫元件內部的toggle方法。

完整程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
      }
      .success {
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .warning {
        background-color: red;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 彈窗元件 -->
      <message ref="msgSuccess" class="success">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>

      <!-- 警告 -->
      <message ref="msgWaring" class="warning">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>警告</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          請輸入使用者名稱
        </template>
      </message>

      <!-- 清空提示欄 -->
      <div class="toolbar">
        <button @click="$bus.$emit('message-close')">
          清空提示欄
        </button>
      </div>
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 新增使用者 -->
      <user-add @add-user="addUser" v-model="userInfo"></user-add>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      //建立事件匯流排
      Vue.prototype.$bus = new Vue();
      //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        // props: ["show"],
        data() {
          return {
            show: false,
          };
        },

        template: `<div class='message-box' v-if="show">
               <!--具名插槽-->
               <slot name="title">預設標題</slot>
              <slot></slot>
              <span class="message-box-close" @click='toggle'>關閉</span>
            </div>`,
        mounted() {
          //給匯流排繫結`message-close`事件
          //也就是監聽是否有`message-close`事件被觸發。
          this.$bus.$on("message-close", () => {
            // this.$emit("close", false);
            //當警告視窗和提示資訊的視窗,展示出來了才關閉。
            if (this.show) {
              this.toggle();
            }
          });
        },
        methods: {
          toggle() {
            this.show = !this.show;
          },
        },
      });

      //新增使用者元件
      Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
              <div>
               <p>
                  <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" ref="inp" />
               </p>
               <button @click="addUser">新增使用者</button>
                </div>
              `,

        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
        mounted() {
          this.$refs.inp.focus();
        },
      });

      // 使用者列表
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
          <div>
                  <p v-if="users.length===0">沒有任何使用者資料</p>
              <ul v-else>
                  <li
                  v-for="(item,index) in users"
                  :key="item.id"
                  :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                  @mousemove="selectItem=item"
                  >
                  編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                  </li>
              </ul>
        </div>
          `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          // isShow: false,
          // showWarn: false, // 控制警告視窗的顯示與隱藏
        },

        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //關閉視窗
          closeWindow(data) {
            this.isShow = data;
            this.showWarn = data;
          },
          //新增使用者的資訊
          addUser() {
            if (this.userInfo) {
              if (this.users.length > 0) {
                this.users.push({
                  id: this.users[this.users.length - 1].id + 1,
                  name: this.userInfo,
                });
                this.userInfo = "";
                //完成使用者新增後,給出相應的提示資訊
                // this.isShow = true;
                this.$refs.msgSuccess.toggle();
              }
            } else {
              // 顯示錯誤警告資訊
              // this.showWarn = true;
              this.$refs.msgWaring.toggle();
            }
          },

          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

下面在對refvm.$refs的使用做一個總結:

  • ref是作為渲染結果被建立的,在初始渲染時不能訪問它們。也就是必須在mounted建構函式中。
  • $refs不是響應式的,不要試圖用它在模板中做資料繫結。

17、過濾器

17.1 過濾器基本使用

過濾器在日常生活中也是比較常見的,例如自來水的過濾等。

Vue中,過濾器的作用就是格式化資料,也就是對資料的過濾處理,比如將字串格式化為首字母大寫

或者將日期格式化為指定的格式等。

下面先看一下自定義過濾器的語法

Vue.filter('過濾器名稱',function(value){
//value引數表示要處理的資料
  //過濾器業務邏輯,最終將處理後的資料進行返回
})

定義好以後可以使用。使用的方式如下:

<div>{{msg|upper}}</div>
<div>{{msg|upper|lower}}</div>

具體的程式如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>過濾器基本使用</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="msg" />
      <div>
          <!--使用過濾器-->
        {{msg|upper}}
      </div>
    </div>
    <script src="vue.js"></script>
    <script>
      //定義過濾器,讓輸入的單詞首字母變成大寫.
      Vue.filter("upper", function (value) {
        //獲取首字母讓其轉換成大寫,然後拼接後面的內容。
        return value.charAt(0).toUpperCase() + value.slice(0);
      });
      const vm = new Vue({
        el: "#app",
        data: {
          msg: "",
        },
      });
    </script>
  </body>
</html>

過濾器在使用的時候,可以採用如下的方式:

<div>{{msg|upper|lower}}</div>

也就是,先對msg中的資料使用upper過濾器,得到的結果在交給lower過濾器進行處理。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>過濾器基本使用</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="msg" />
      <div>
        {{msg|upper}}
      </div>
      <div>
        {{msg|upper|lower}}
      </div>
    </div>
    <script src="vue.js"></script>
    <script>
      //定義過濾器,讓輸入的單詞首字母變成大寫.
      Vue.filter("upper", function (value) {
        //獲取首字母讓其轉換成大寫,然後拼接後面的內容。
        return value.charAt(0).toUpperCase() + value.slice(0);
      });
      Vue.filter("lower", function (value) {
        return value.charAt(0).toLowerCase() + value.slice(0);
      });
      const vm = new Vue({
        el: "#app",
        data: {
          msg: "",
        },
      });
    </script>
  </body>
</html>

上面定義的顧慮器是全域性的過濾器,當然也可以定義區域性過濾器。

區域性過濾器只能在其所定義的元件內使用。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>過濾器基本使用</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="msg" />
      <div>
        {{msg|upper}}
      </div>
      <div>
        {{msg|upper|lower}}
      </div>
    </div>
    <script src="vue.js"></script>
    <script>
      //定義過濾器,讓輸入的單詞首字母變成大寫.
      //   Vue.filter("upper", function (value) {
      //     //獲取首字母讓其轉換成大寫,然後拼接後面的內容。
      //     return value.charAt(0).toUpperCase() + value.slice(0);
      //   });
      Vue.filter("lower", function (value) {
        return value.charAt(0).toLowerCase() + value.slice(0);
      });
      const vm = new Vue({
        el: "#app",
        data: {
          msg: "",
        },
        //區域性過濾器
        filters: {
          upper: function (value) {
            return value.charAt(0).toUpperCase() + value.slice(0);
          },
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們通過fileters定義了一個區域性的過濾器upper.

在前面,我們也說過Vue例項本身就是一個元件。

17.2 帶引數的過濾器

帶引數的過濾器定義如下:

Vue.filter('format',function(value,arg1){
	//value表示要過濾的資料。
	//arg1,表示傳遞過來的引數

})

使用的方式如下

<div>
 {{data|format(`yyyy-MM-dd`)}}
</div>

要處理的資料data交給了過濾器中回撥函式的value引數,yyyy-MM-dd交給了arg1.

如下程式碼:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>過濾器引數</title>
  </head>
  <body>
    <div id="app">
      <div>
        {{date|format('abc','hello')}}
      </div>
    </div>
    <script src="vue.js"></script>
    <script>
      Vue.filter("format", function (value, arg, arg1) {
        console.log(arg, arg1);
        return value;
      });
      const vm = new Vue({
        el: "#app",
        data: {
          date: new Date(),
        },
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們定義了format過濾器,然後在使用的時候,我們是將date日期資料給了value

abc這個字串給了arg,hello給了arg1.

下面,我們把日期給具體的處理一下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>過濾器引數</title>
  </head>
  <body>
    <div id="app">
      <div>
        {{date|format('yyyy-MM-dd')}}
      </div>
    </div>
    <script src="vue.js"></script>
    <script>
      Vue.filter("format", function (value, arg, arg1) {
        let result = "";
        result +=
          value.getFullYear() +
          "-" +
          (value.getMonth() + 1) +
          "-" +
          value.getDate();
        return result;
      });
      const vm = new Vue({
        el: "#app",
        data: {
          date: new Date(),
        },
      });
    </script>
  </body>
</html>

18、自定義指令

18.1 自定義指令基本用法

為什麼需要自定義指令呢?

因為內建指令不滿足需求。

下面看一下基本的建立自定義指令語法:

Vue.directive('focus',{
              inserted:function(el){
      //獲取元素焦點
     el.focus();
		}
              
   })

自定義指令用法

<input type="text" v-focus>

下面看一下具體的程式碼。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>自定義指令基本使用</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-focus />
    </div>
    <script src="vue.js"></script>
    <script>
        
      Vue.directive("focus", {
        inserted: function (el) {
          //el:表示指令所繫結的元素
          el.focus();
        },
      });
      const vm = new Vue({
        el: "#app",
        data: {},
      });
    </script>
  </body>
</html>

在上面的程式碼中,我們通過directive方法建立了一個focus指令。

在使用該指令的時候,一定要加上v-的形式。

inserted表示的是指令的鉤子函式,含義是:被繫結元素插入父節點時呼叫。

18.2 自定義指令-帶引數

帶引數的自定義指令建立的語法(改變元素背景色)

Vue.directive('color',{
    inserted:function(el,binding){
        //binding表示傳遞過來的引數
        el.style.backgroundColor=binding.value.color;
    }
})

指令的用法

<input type="text" v-color='{color:"orange"}' />

下面,看一下完整的程式碼案例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>自定義指令帶引數</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-color="msg" />
    </div>
    <script src="vue.js"></script>
    <script>
      //自定義指令-帶引數
      Vue.directive("color", {
        bind: function (el, binding) {
          el.style.backgroundColor = binding.value.color;
        },
      });
      const vm = new Vue({
        el: "#app",
        data: {
          msg: {
            color: "blue",
          },
        },
      });
    </script>
  </body>
</html>

通過上面的程式碼,可以看到定義了一個color的指令,在使用的時候傳遞了msg物件。

所以這個物件會給binding這個引數,我們通過這個引數的value 屬性獲取msg物件中的color屬性的值,然後用來設定文字框的背景色。

這裡使用了bind這個鉤子函式:只調用一次,第一次繫結指令到元素時呼叫,我們可以在此繫結只執行一次的初始化動作。

18.3 自定義區域性指令

區域性指令的基本語法:

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

Vue例項中新增directives

具體實現的程式碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>區域性指令</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-color="msg" />
    </div>
    <script src="vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        data: {
          msg: {
            color: "red",
          },
        },
        directives: {
          color: {
            bind: function (el, binding) {
              el.style.backgroundColor = binding.value.color;
            },
          },
        },
      });
    </script>
  </body>
</html>

區域性指令只在所定義的元件中使用。

19、渲染函式

Vue推薦在絕大數情況下使用模板來建立你的HTML。然後在一些場景中,你真的需要JavaScript的完全程式設計的能力,也就是使用javaScript來建立HTML,這時你可以用渲染函式,它比模板更接近編譯器。

這裡我們先來做一個基本的瞭解,為後期的深入學習打好一個基礎。

下面先看一下render函式的基本結構。

render:function(createElement){
    //createElement函式返回的結果為VNode. VNode就是虛擬dom,用js物件來模擬真實的DOM.
    retrun createElement(
      tag, //標籤名稱
       data,// 傳遞資料
       children //子節點陣列 
    )
    
}

下面我們在使用者管理這個案例中,使用render函式來建立一個元件。

具體的程式碼如下:

 // heading元件
      //<heading :level="1">{{title}}</heading> //這時要建立的元件
      // <h2 title=""></h2> //這時上面的元件最終渲染的結果
      Vue.component("heading", {
        props: {
          level: {
            type: String,
            required: true,
          },
        },
        render(h) { //h 就是createElement函式
          return h(
            "h" + this.level, //引數1,表示要建立的元素
            this.$slots.default //引數3,子節點VNode陣列。(這裡沒有使用引數2,{{tile}}就是一個子元素)
          );
        },
      });

接下來就可以使用heading元件了。

  <!-- 使用render函式建立的頭部元件 -->
      <heading level="1">
        {{title}}
      </heading>

當然,這裡需要在data中定義title屬性。

data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          title: "使用者管理",
          // isShow: false,
          // showWarn: false, // 控制警告視窗的顯示與隱藏
        },

完整程式碼如下(24、render函式.html):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>列表渲染</title>
    <style>
      .actived {
        background-color: #dddddd;
      }
      .message-box {
        padding: 10px 20px;
      }
      .success {
        background-color: #4fc;
        border: 1px solid #42b;
      }
      .warning {
        background-color: red;
        border: 1px solid #42b;
      }
      .message-box-close {
        float: right;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 彈窗元件 -->
      <message ref="msgSuccess" class="success">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>恭喜</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          新增使用者成功
        </template>
      </message>

      <!-- 警告 -->
      <message ref="msgWaring" class="warning">
        <!-- titile的插槽 -->
        <template v-slot:title>
          <h2>警告</h2>
        </template>
        <!-- 預設插槽 -->
        <template>
          請輸入使用者名稱
        </template>
      </message>

      <!-- 使用render函式建立的頭部元件 -->
      <heading level="1">
        {{title}}
      </heading>
      <!-- 清空提示欄 -->
      <div class="toolbar">
        <button @click="$bus.$emit('message-close')">
          清空提示欄
        </button>
      </div>
      <!-- 批量更新身高 -->
      <p>
        <input type="text" v-model.number="height" />
        <button @click="batchUpdate">批量更新使用者身高</button>
      </p>
      <!-- 新增使用者 -->
      <user-add @add-user="addUser" v-model="userInfo"></user-add>
      <!-- 使用者列表元件 -->
      <user-list :users="users"></user-list>

      <p>
        總人數:{{totalCount}}
      </p>
    </div>
    <script src="vue.js"></script>
    <script>
      //建立事件匯流排
      Vue.prototype.$bus = new Vue();

      // heading元件
      //<heading :level="1">{{title}}</heading> //這時要建立的元件
      // <h2 title=""></h2> //這時上面的元件最終渲染的結果
      Vue.component("heading", {
        props: {
          level: {
            type: String,
            required: true,
          },
        },
        render(h) {
          return h(
            "h" + this.level, //引數1,表示要建立的元素
            this.$slots.default //引數3,子節點VNode陣列。(這裡沒有使用引數2,{{tile}}就是一個子元素)
          );
        },
      });

      //建立彈出的元件
      Vue.component("message", {
        //show表示的含義,控制彈出視窗的顯示與隱藏。
        //slot:表示佔坑。也就是視窗中的內容,是通過外部元件傳遞過來的。
        // props: ["show"],
        data() {
          return {
            show: false,
          };
        },

        template: `<div class='message-box' v-if="show">
               <!--具名插槽-->
               <slot name="title">預設標題</slot>
              <slot></slot>
              <span class="message-box-close" @click='toggle'>關閉</span>
            </div>`,
        mounted() {
          //給匯流排繫結`message-close`事件
          //也就是監聽是否有`message-close`事件被觸發。
          this.$bus.$on("message-close", () => {
            // this.$emit("close", false);
            //當警告視窗和提示資訊的視窗,展示出來了才關閉。
            if (this.show) {
              this.toggle();
            }
          });
        },
        methods: {
          toggle() {
            this.show = !this.show;
          },
        },
      });

      //新增使用者元件
      Vue.component("user-add", {
        // data() {
        //   return {
        //     userInfo: "",
        //   };
        // },
        props: ["value"],
        template: `
              <div>
               <p>
                  <input type="text" :value="value" @input="onInput" v-on:keydown.enter="addUser" ref="inp" />
               </p>
               <button @click="addUser">新增使用者</button>
                </div>
              `,

        methods: {
          addUser() {
            //將輸入的使用者資料通知給父元件,來完成新增使用者操作.
            // this.$emit("add-user", this.userInfo);
            this.$emit("add-user");
            // this.userInfo = "";
          },
          onInput(e) {
            this.$emit("input", e.target.value);
          },
        },
        mounted() {
          this.$refs.inp.focus();
        },
      });

      // 使用者列表
      Vue.component("user-list", {
        data() {
          return {
            selectItem: "",
          };
        },
        props: {
          users: {
            type: Array,
            default: [],
          },
        },
        template: `
          <div>
                  <p v-if="users.length===0">沒有任何使用者資料</p>
              <ul v-else>
                  <li
                  v-for="(item,index) in users"
                  :key="item.id"
                  :style="{backgroundColor:selectItem===item?'#dddddd':'transparent'}"
                  @mousemove="selectItem=item"
                  >
                  編號:{{item.id}} 姓名:{{item.name}}---身高:{{item.height}}
                  </li>
              </ul>
        </div>
          `,
      });
      new Vue({
        el: "#app",
        data: {
          num: 100,
          totalCount: 0,
          users: [],
          height: 0,
          userInfo: "abc",
          title: "使用者管理",
          // isShow: false,
          // showWarn: false, // 控制警告視窗的顯示與隱藏
        },

        //元件例項已建立時
        async created() {
          const users = await this.getUserList();
          this.users = users;
          //批量更新使用者身高
          this.batchUpdate();
        },
        methods: {
          //關閉視窗
          closeWindow(data) {
            this.isShow = data;
            this.showWarn = data;
          },
          //新增使用者的資訊
          addUser() {
            if (this.userInfo) {
              if (this.users.length > 0) {
                this.users.push({
                  id: this.users[this.users.length - 1].id + 1,
                  name: this.userInfo,
                });
                this.userInfo = "";
                //完成使用者新增後,給出相應的提示資訊
                // this.isShow = true;
                this.$refs.msgSuccess.toggle();
              }
            } else {
              // 顯示錯誤警告資訊
              // this.showWarn = true;
              this.$refs.msgWaring.toggle();
            }
          },

          //批量更新身高,動態的給users中新增身高屬性
          batchUpdate() {
            this.users.forEach((c) => {
              //   c.height = this.height;
              //   Vue.set(c, "height", this.height);
              this.$set(c, "height", this.height);
            });
          },

          getTotal: function () {
            console.log("methods");
            return this.users.length + "個";
          },
          getUserList: function () {
            return new Promise((resolve) => {
              setTimeout(() => {
                resolve([
                  {
                    id: 1,
                    name: "張三",
                  },
                  {
                    id: 2,
                    name: "李四",
                  },
                  {
                    id: 3,
                    name: "老王",
                  },
                ]);
              }, 2000);
            });
          },
        },
        watch: {
          users: {
            immediate: true, //立即執行
            handler(newValue, oldValue) {
              this.totalCount = newValue.length + "個人";
            },
          },
        },
      });
    </script>
  </body>
</html>

虛擬DOM

Vue通過建立一個虛擬DOM來追蹤自己要如何改變真實DOM.

createElement引數

前面說過,createElement函式有三個引數。

createElement(
  //{string |Object|Function}
    //第一個引數,可以是字串,也可以是物件或者是函式
    ‘div’
    ,
    // 第二個引數是物件,表示的是一個與模板中屬性對應的資料物件。該引數可選
    {
        
    },
    //第三個引數是一個數組,表示的是子節點陣列
    [
        
    ]
)
    

下面,給heading元件新增第一個屬性。

  <!-- 使用render函式建立的頭部元件 -->
      <heading level="1" :title="title">
        {{title}}
      </heading>

在上面的程式碼中,我們給heading元件動態添加了一個title屬性。而我們知道heading元件,最終渲染成的是h1的元素,最終效果為:<h1 title='aaa'>的形式。

 // heading元件
      //<heading :level="1">{{title}}</heading> //這時要建立的元件
      // <h2 title=""></h2> //這時上面的元件最終渲染的結果
      Vue.component("heading", {
        props: {
          level: {
            type: String,
            required: true,
          },
          title: {
            type: String,
            default: "",
          },
        },
        render(h) {
          return h(
            "h" + this.level, //引數1,表示要建立的元素
            { attrs: { title: this.title } }, //引數2
            this.$slots.default //引數3,子節點VNode陣列。(這裡沒有使用引數2,{{tile}}就是一個子元素)
          );
        },
      });

在上面的程式碼中,我們在render函式中給h函式添加了第二個引數,給最終生成的元素添加了attrs屬性。

20、函式式元件

元件沒有管理任何狀態,也沒有監聽任何傳遞給它的狀態,也沒有生命週期方法時,可以將元件標記為functional.這意味它無狀態(沒有響應式資料),也沒有例項(沒有this上下文)

因為只是函式,所以渲染的開銷相對來說,較小。

函式化的元件中的 Render 函式,提供了第二個引數 context 作為上下文,data、props、slots、children 以及 parent 都可以通過 context 來訪問。

這塊內容簡單瞭解一下就可以。

21、混入

混入(mixin)提供了一種非常靈活的方式,來分發Vue元件中的可複用功能,一個混入物件可以包含任意元件選項。當元件使用混入物件時,所有混入物件的選項被“混合”進入該元件本身的選項。

// 定義一個混入物件
var myMixin={
    created:function(){
        this.hello()
    },
    methods:{
        hello:function(){
            console.log('hello world')
        }
    }
}
Vue.component('comp',{
    mixins:[myMixin]
})

“混入”可以提高元件的複用功能,例如:上面所寫的hello這個方法,不僅在一個元件中使用,還會

在其它元件中使用.那麼,我們的處理方式就是,可以將hello 這個方法單獨定義在一個地方,如果某個元件想要使用,可以直接將該方法注入到元件中。

22、外掛

前面我們講解的混入,元件封裝等都可以提高元件的複用功能。

但是這種方式不適合分發,也就是不適合將這些內容上傳到github上,npm上。而這種情況最適合通過外掛來實現。

外掛通常用來為Vue新增全域性功能。外掛的功能範圍一般有下面幾種:

  • 新增全域性方法或者屬性。例如:'element'
  • 新增全域性資源
  • 通過全域性混入來新增一些元件選項。例如vue-router
  • 新增vue例項方法,通過把它們新增到Vue.prototype上實現
  • 一個庫,提供自己的API,同時提供上面提到的一個或多個功能,例如vue-router

外掛宣告

Vue.js 的外掛應該暴露一個 install 方法。這個方法的第一個引數是 Vue 構造器,第二個引數是一個可選的選項物件:

MyPlugin.install = function (Vue, options) {
  // 1. 新增全域性方法或 property
  Vue.myGlobalMethod = function () {
    // 邏輯...
  }

  // 2. 新增全域性資源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 邏輯...
    }
    ...
  })

  // 3. 注入元件選項
  Vue.mixin({
    created: function () {
      // 邏輯...
    }
    ...
  })

  // 4. 新增例項方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 邏輯...
  }
}

https://www.cnblogs.com/luozhihao/p/7414419.html

23、vue-cli使用

npm install -g @vue/cli

通過使用vue-clie建立專案。