vue 快速入門 系列 —— vue 的基礎應用(上)

彭加李 發表於 2021-04-07

其他章節請看:

vue 快速入門 系列

vue 的基礎應用(上)

Tip: vue 的基礎應用分上下兩篇,上篇是基礎,下篇是應用

初步認識 vue一文中,我們已經寫了一個 vue 的 hello-world。對 vue 已經有了一個大概的印象。

接下來我們應該掌握 vue 的最基礎知識,學會 vue 的基本應用。

比較好的方法就是花個幾天的時間將 vue 官網的基礎篇儘量走一遍,寫一寫例子。以下是 vue 2.x 的基礎篇的目錄:

- 教程 2.x
    - 基礎
        - 安裝
        - 介紹
        - Vue 例項
        - 模板語法
        - 計算屬性和偵聽器
        - Class 與 Style 繫結
        - 條件渲染
        - 列表渲染
        - 事件處理
        - 表單輸入繫結
        - 元件基礎

:如果你像筆者一樣,只有 jQuery 開發的經驗,在看基礎篇的過程種,肯定會有很多的不懂之處,先看下去,把能看懂的先看完。

本文記錄的是筆者當初看 vue 基礎篇遇到的一些重要的、不好理解的知識點,算是基礎篇的一個補充或筆記。

Vue 例項

通過 new Vue() 來建立一個例項,其中 el 選項提供一個在頁面上已存在的 DOM 元素作為 Vue 例項的掛載目標。

vue 例項會經歷一系列的過程,比如資料偵測、模板編譯、渲染到檢視等,這就是 vue 例項的生命週期,vue 對外提供了生命週期的鉤子函式,這就允許我們在 vue 的各個階段插入一些我們的邏輯。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='vue.js'></script>
</head>
<body>
<div id='app'>
    <p>
      {{message}}
    </p>
</div>
<script>
// 通過 new Vue() 建立一個例項
var app = new Vue({
  // 提供一個在頁面上已存在的 DOM 元素作為 Vue 例項的掛載目標。
  // 可以是 CSS 選擇器,也可以是一個 HTMLElement 例項。
  el: '#app',
  // Vue 例項的資料物件
  data:{
    message: 'hello',
  },
  // 生命週期
  created: function(){
    console.log('例項被建立時觸發')
  },
  mounted: function(){
    console.log('被掛載時觸發')
  }
  
})

</script>
</body>
</html>

Tip: 後續不在提供完整的程式碼,省略 head、body 等。

模板語法

前文說過 vue 是宣告式操作 dom 的框架。我們只需要描述狀態與 dom 之間的對映關係即可,狀態到檢視的轉換,框架會幫我們做,狀態改變,檢視也會自動更新。

而 vue 是通過模板來描述狀態與 dom 之間的對映關係。所以模板的知識點稍微會多一點。

條件渲染

用 key 管理可複用的元素

Vue 會盡可能高效地渲染元素,通常會複用已有元素而不是從頭開始渲染。

<div id='app'>
  <template v-if="loginType === 'username'">
    <label>Username</label>
    <input placeholder="Enter your username">
  </template>
  <template v-else>
    <label>Email</label>
    <input placeholder="Enter your email address">
  </template>
  <button @click='toggleHandle'>切換</button>
</div>

<script>
var app = new Vue({
  el: '#app',
  data:{
    loginType: 'username',
  },
  methods: {
    toggleHandle: function(){
      this.loginType = this.loginType === 'username' ? 'email' : 'username';
    }
  }
})
</script>

在上面的程式碼中切換 loginType 將不會清除使用者已經輸入的內容。因為兩個模板使用了相同的元素,<input> 不會被替換掉——僅僅是替換了它的 placeholder。如果需要表達“這兩個元素是完全獨立的,不要複用它們”,只需新增一個具有唯一值的 key attribute 即可。

// 給 input  新增 key 屬性
<input placeholder="Enter your username" key="username-input">
<input placeholder="Enter your email address" key="email-input">    

v-if vs v-show

v-if 是真正的條件渲染;v-show 只是簡單地切換元素的 CSS property display;

例如下面這種場景,點選一個新建按鈕,顯示彈框元件(custom-dialog),彈框中有一些 input 輸入框,如果需要每次點選新建,彈框元件整個都重新生成,則可以使用 v-if;否則在彈框中輸入了文字,通過 v-show 隱藏彈框,下次在顯示時,可能之前輸入的文字、錯誤資訊都還存在。

<custom-dialog v-if='isShow'></custom-dialog>
<button @click='clickHandle'>新建</button>

表單輸入繫結

v-model:在表單控制元件或者元件上建立雙向繫結。

應用在表單控制元件上,請看示例:

<div id='app'>
  <input v-model="message" placeholder="edit me"> <!-- {1} -->
  <!-- 
  <input type="text" :value='message' @input='handleInput' placeholder="edit me"> // {2}
  -->
  <p>Message is: {{ message }}</p>
</div>
<script>
var app = new Vue({
  el: '#app',
  data:{
    message: 'a'
  },
  methods: {
    handleInput: function(e){
      this.message = e.target.value
    }
  }
})
</script>

v-model 本質上不過是語法糖。行{1} 的本質其實是 行{2}。

v-model 在元件中的使用請看下文。

元件基礎

上面介紹的其實是基礎中最簡單的部分。而元件是基礎篇中最應該掌握的部分。

Vue.component()

可以通過 Vue.component() 註冊或獲取全域性元件。請看示例:

   <div id='app'>
    <p>{{message}}</p>
    <button-counter></button-counter>
    <button-counter></button-counter>
  </div>
  <script>

  // 定義一個名為 button-counter 的新元件
  Vue.component('button-counter', {
    // 一個元件的 data 選項必須是一個函式
    // 這樣,每個元件的資料都是獨立的
    data: function () {
      return {
        count: 0
      }
    },
    template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
  })

  var app = new Vue({
    el: '#app',
    data: {
      message: 'hello'
    }
  })
  </script>

元件是可複用的 Vue 例項,且帶有一個名字:在這個例子中是 <button-counter>

因為元件是可複用的 Vue 例項,所以它們與 new Vue() 接收相同的選項,例如 data、computed、watch、methods 以及生命週期鉤子等。僅有的例外是像 el 這樣根例項特有的選項。

通常一個應用會以一棵巢狀的元件樹的形式來組織,這樣就能完成一個功能很複雜的頁面。

props

props 用於接收來自父元件的資料。請看示例:

  <div id='app'>
    <!-- <button-counter v-bind:msg='message'></button-counter> -->
    <!-- 縮寫 -->
    <button-counter :msg='message'></button-counter>
  </div>
  <script>

  Vue.component('button-counter', {
    props: ['msg'],
    template: `<div>
                來自父元件的資訊: {{msg}}
              </div>`
  })

  var app = new Vue({
    el: '#app',
    data: {
      message: 'hello'
    }
  })
  </script>

v-bind:msg='message' 可以縮寫成 :msg='message'

v-on

v-on 用在自定義元素元件上時,可以監聽子元件觸發的自定義事件。請看示例:

<div id='app'>
    number: {{count}}
    <!-- 父元件給子元件註冊了事件 chang-count,事件的回撥方法是 changCount -->
    <button-counter v-on:chang-count='changCount'></button-counter>
  </div>
  <script>

  Vue.component('button-counter', {
    data: function(){
      return {
        num: 10
      }
    },
    template: `<div>
                <!-- v-on:click 縮寫 @click -->
                <button @click='triggerHandle'>觸發</button>
                
              </div>`,
    methods: {
      triggerHandle: function(){
        // 觸發事件
        this.$emit('chang-count', this.num++);
      }
    }
  })

  var app = new Vue({
    el: '#app',
    data: {
      count: 0
    },
    methods: {
      changCount: function(v){
        this.count = v;
      }
    }
  })
  </script>

v-on 的語法糖是 @。

v-model

v-model 在表單控制元件或者元件上建立雙向繫結。上文已經介紹瞭如何在表單上使用。而用在元件上,原理是相同的。請看示例:

 <div id='app'>
    number: {{count}}
    <!-- 父元件給子元件註冊了事件 chang-count,事件的回撥方法是 changCount -->
    <custom-input v-model="count"></custom-input>                              <!-- {20} -->
    <!-- <custom-input :value="count" @input='changCount'></custom-input> -->  <!-- {21} -->
  </div>
  <script>

  Vue.component('custom-input', {
    props: ['value'],
    data: function(){
      return {
        msg: this.value
      }
    },
    template: `<div>
                <input :value='msg' @input='inputHandle'/>
              </div>`,
    methods: {
      inputHandle: function(e){
        this.msg = e.target.value
        // 觸發事件
        this.$emit('input', this.msg);
      }
    }
  })

  var app = new Vue({
    el: '#app',
    data: {
      count: 10
    },
    methods: {
      changCount: function(v){
        this.count = v;
      }
    }
  })
  </script>

行{20} 與 行{21} 等效。

非父子元件通訊

props 用於父元件給子元件傳遞資料;

子元件給父元件傳遞資料可以通過事件(例如 vm.$emit());

但非父子之間如何通訊?

對於 $dispatch 和 $broadcast 最簡單的升級方式就是:通過使用事件中心,允許元件自由交流,無論元件處於元件樹的哪一層。由於 Vue 例項實現了一個事件分發介面,你可以通過例項化一個空的 Vue 例項來實現這個目的。—— vue 官網

簡單來說,就是使用一個空的 Vue 例項作為中央事件匯流排,可以幫助我們解決兄弟元件之間的通訊。請看示例:

<div id='app'>
    <component-a></component-a>
    <component-b></component-b>
  </div>
  <script>

Vue.component('componentA', {
    data: function(){
      return {
        msg: '我是 componentA'
      }
    },
    template: `<div>
                message: {{msg}}
                <button @click='send'>給兄弟傳送資料</button>
              </div>`,
    created: function(){
      var that = this;
      // 給事件中心註冊事件
      bus.$on('on-messageA', function(msg){
        console.log('received: ' + msg)
        that.msg = msg
      })
    },
    methods: {
      send: function(e){
        // 給事件中心觸發事件
        bus.$emit('on-messageB', 'from componentA')
      }
    }
  })

  Vue.component('componentB', {
    data: function(){
      return {
        msg: '我是 componentB'
      }
    },
    template: `<div>
                message: {{msg}}
                <button @click='send'>給兄弟傳送資料</button>
              </div>`,
    created: function(){
      var that = this;
      bus.$on('on-messageB', function(msg){
        that.msg = msg
      })
    },
    methods: {
      send: function(e){
        console.log('from componentB')
        bus.$emit('on-messageA', 'from componentB')
      }
    }
  })
  // 事件中心
  var bus = new Vue()
  var app = new Vue({
    el: '#app'
  })
  </script>

這個例子中定義了兩個元件,首先用 new Vue() 建立一箇中央事件匯流排(bus),類似一箇中介,接著元件(componentA、componentB)給 bus 註冊事件,當點選按鈕時觸發 bus 的事件,並傳遞資料。

這種方式其實可以實現任何元件之間的通訊,包括父子、兄弟、跨級等,因為在 bus 看來,我(bus)就是中介,其他元件只是我的客戶。

Tip:專案比較大時,通常會使用 vuex,後續會介紹 vuex 這個狀態管理工具。

父元件和子元件

vm.$parent 可以取得父元件;通過 vm.$children 取得當前例項的直接子元件,並通過 vm.$refs 取得指定子元件。請看示例:

<div id='app'>
    <component-a ref='comA'></component-a>
    <component-a ref='comB'></component-a>
  </div>
  <script>

  Vue.component('componentA', {
    data: function(){
      return {
        msg: 'hello'
      }
    },
    template: `<div>
                message: {{msg}}
              </div>`,
    watch: {
      msg: function(){
        // 通過 vm.$parent 取得父元件
        console.log(this.$parent.name)
        // 通過 ref 指定子元件索引名稱
        // 通過 vm.$refs 取得子元件
        console.log(this.$parent.$refs.comA.msg)
      }
    }
  })

  var app = new Vue({
    el: '#app',
    data: {
      name: '我是父'
    }
  })
  // 通過 vm.$children 直接更改子元件的資料
  console.log(app.$children[1].msg = 'hello2')
  </script>

通過插槽分發內容

插槽也可以叫 slot 內容分發。請看示例:

  <div id='app'>
    <component-a ref='comA'>
      <p>子元素的 slot 插槽將會被我替換</p>     <!-- {30} -->
    </component-a>
  </div>
  
  <script>
  Vue.component('componentA', {
    template: `<div>
                hello
                <slot></slot>
              </div>`,
    
  })

  var app = new Vue({
    el: '#app',
  })
  </script>

我們在元件A中定義了一個插槽(<slot></slot>),父元件中的 p(行{30})將會替換子元件的 slot。

Tip: props、事件以及 slot 內容分發構成了 Vue 元件的 3 個 API 來源。

作用域插槽,概念不太好理解,直接看示例:

  <div id='app'>
    <component-a>
      <!-- slot-scope 用於將元素或元件表示為作用域插槽 -->
      <template slot-scope='props'>
        <p>來自父元件</p>
        <p>{{props.msg}}</p>
      </template>
    </component-a>
  </div>
  
  <script>
  Vue.component('componentA', {
    data: function(){
      return {
        msg: '來自子元件的內容'
      }
    },
    template: `<div>
                hello
                <slot :msg='msg'></slot>
              </div>`,
    
  })

  var app = new Vue({
    el: '#app',
  })
  </script>

作用域插槽,就是讓插槽的內容能夠訪問子元件中才有的資料。

:由於筆者的 vue 是 2.5.x,所以用的是 slot-scope。而在 2.6.x 中,scope、slot和slot-scope 都推薦使用 v-scope。

其他

過濾器

Vue.js 允許你自定義過濾器,可被用於一些常見的文字格式化。請看示例:

  <div id='app'>
    <p>{{msg | toUpperCase}}</p>
  </div>
  <script>
 
  var app = new Vue({
    el: '#app',
    data: {
      msg: 'hello'
    },
    // 過濾器
    filters: {
      toUpperCase: function(v){
        return v.toUpperCase()
      }
    }
  })
  </script>

.sync 修飾符

在有些情況下,我們可能需要對一個 prop 進行“雙向繫結”。不幸的是,真正的雙向繫結會帶來維護上的問題,因為子元件可以變更父元件,且在父元件和子元件都沒有明顯的變更來源。這也是為什麼我們推薦以 update:myPropName 的模式觸發事件取而代之。—— vue 官網

簡單來說,父元件給子元件傳遞了一個屬性,子元件不要直接更改父元件的這個屬性,而應該通知父元件,讓父元件自己去更改這個屬性。請看示例:

<div id='app'>
   父元件: name = {{name}}
   <!-- 
     當在父級元件監聽事件的時候,我們可以通過 $event 訪問到被丟擲的這個值 
   -->
   <custom :name="name" v-on:update:name="updateName($event)"></custom> <!-- {40} -->
   <!-- 相等 -->
   <!-- <custom :name.sync="name"></custom> -->                         <!-- {41} --> 
   
  </div>
  <script>
  let seed = 1;
  Vue.component('custom', {
    props: ['name'],
    template: `
      <div>
        <p>子元件: name = {{name}}</p>
        <p><button @click='handleClick'>change name</button></p>
      </div>
    `,
    methods: {
      handleClick: function(){
        // update:name 就是一個事件名
        // 改為 update--name 也可以
        this.$emit('update:name', seed++)
      }
    }
  })
  const app = new Vue({
    el: '#app',
    data: {
      name: 'hello'
    },
    methods: {
      updateName: function(v){
        this.name = v
      }
    }
  })
  </script>

為了方便起見,我們為這種模式(行{40})提供一個縮寫,即 .sync 修飾符(行{41})

其他章節請看:

vue 快速入門 系列