Vue知識總結(1)

image.png

Vue是一款高度封裝的開箱即用的一棧式的前端框架,既可以結合webpack進行編譯式前端開發,也適用基于gulp、grunt等自動化工具直接掛載至全局window使用。本文成文于Vue2.4.x版本發布之初,筆者生產環境當前使用的最新版本為2.5.2。在經歷多個前端重度交互項目的開發實踐之后,筆者結合官方文檔對Vue技術棧進行了全面的梳理、歸納和注解,因此本文可以作為Vue2官方tutorial的補充性讀物。建議暫不具備Vue2開發經驗的同學,完成官方tutorial的學習之后再行閱讀本文。

Vue2.2.x之后,Vue框架及其技術棧功能日趨完善,Vue更加貼近W3C技術規范(例如實現仍處于W3C草案階段的<template><slot>is等新特性,提供了良好易用的模板書寫環境),并且技術棧和開源生態更加完整和易于配置,將React中大量需要手動編碼處理的位置,整合成最佳實踐并抽象為簡單的語法糖(比如Vuex中提供的store的模塊化特性),讓開發人員始終將精力聚焦于業務邏輯本身。

Vue2的API結構相比Angular2更加簡潔,可以自由的結合TypeScript或是ECMAScript6使用,并不特定于具體的預處理語言去獲得最佳使用體驗,框架本身的特性也并不強制依賴于各類炫酷的語法糖。Vue2總體是一款非常輕量的技術棧,設計實現上緊隨W3C技術規范,著力于處理HTML模板組件化事件和數據的作用域分離多層級組件通信三個單頁面前端開發當中的重點問題。本文在行文過程中,穿插描述了Angular、React等前端框架的異同與比較,供徘徊于各類前端技術選型的開發人員參考。

Vue與Angular的比較

組件化

Angular的設計思想照搬了Java Web開發當中MVC分層的概念,通過Controller切割并控制頁面作用域,然后通過Service來實現復用,是一種對頁面進行縱向分層的解耦思想。而Vue允許開發人員將頁面抽象為若干獨立的組件,即將頁面DOM結構進行橫向切割,通過組件的拼裝來完成功能的復用、作用域控制。每個組件只提供props作為單一接口,并采用Vuex進行state tree的管理,從而便捷的實現組件間狀態的通信與同步。

Angular在1.6.x版本開始提供component()方法和Component Router來提供組件化開發的體驗,但是依然需要依賴于controllerservice的劃分,實質上依然沒有擺脫MVC縱向分層思想的桎梏。

雙向綁定與響應式綁定

Vue遍歷data對象上的所有屬性,并通過原生Object.defineProperty()方法將這些屬性轉換為getter/setter只支持IE9及以上瀏覽器)。Vue內部通過這些getter/setter追蹤依賴,在屬性被修改時觸發相應變化,從而完成模型到視圖的雙向綁定。每個Vue組件實例化時,都會自動調用$watch()遍歷自身的data屬性,并將其記錄為依賴項,當這些依賴項的setter被觸發時會通知watcher重新計算新值,然后觸發Vue組件的render()函數重新渲染組件。

image.png

與Aangular雙向數據綁定不同,Vue組件不能檢測到實例化后data屬性的添加、刪除,因為Vue組件在實例化時才會對屬性執行getter/setter處理,所以data對象上的屬性必須在實例化之前存在,Vue才能夠正確的進行轉換。因而,Vue提供的并非真正意義上的雙向綁定,更準確的描述應該是單向綁定,響應式更新,而Angular即可以通過$scope影響view上的數據綁定,也可以通過視圖層操作$scope上的對象屬性,屬于真正意義上的視圖與模型的雙向綁定

var vm = new Vue({
  data:{
    a:1
  }
})
vm.a = 1  // 響應的
vm.b = 2 // 非響應的

因此,Vue不允許在已經實例化的組件上添加新的動態根級響應屬性(即直接掛載在data下的屬性),但是可以使用Vue.set(object, key, value)方法添加響應式屬性。

Vue.set(vm.someObject, "b", 2)

// vm.$set()實例方法是Vue.set()全局方法的別名
this.$set(this.someObject, "b",2)

// 使用Object.assign()或_.extend()也可以添加響應式屬性,但是需要創建
// 同時包含原屬性、新屬性的對象,從而有效觸發watch()方法
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

Vue對DOM的更新是異步的,觀察到數據變化后Vue將開啟一個隊列,緩沖在同一事件循環(Vue的event loop被稱為tick* [t?k] n.標記,記號*)中發生的所有數據變化。如果同一個watcher被多次觸發,只會向這個隊列中推入一次。

Vue內部會通過原生JavaScript的Promise.thenMutationObserversetTimeout(fn, 0)來執行異步隊列當中的watcher。

在需要人為操作DOM的場景下,為了在Vue響應數據變化之后再更新DOM,可以手動調用Vue.nextTick(callback),并將DOM操作邏輯放置在callback回調函數中,從而確保響應式更新完成之后再進行DOM操作。

<div id="example">{{message}}</div>

<script>
// 使用Vue實例上的.$nextTick()
var vue = new Vue({
  el: "#example",
  data: {
    message: "123"
  }
})
vue.message = "new message" // 更改數據
vue.$el.textContent === "new message" // false
vue.nextTick(function () {
  vm.$el.textContent === "new message" // true
})
</script>

<script>
// 組件內使用vm.$nextTick(),不需要通過全局Vue,且回調函數中this自動指向當前Vue實例
Vue.component("example", {
  template: "<span>{{ message }}</span>",
  data: function () {
    return {
      message: "沒有更新"
    }
  },
  methods: {
    updateMessage: function () {
      this.message = "更新完成"
      console.log(this.$el.textContent) // 沒有更新
      this.$nextTick(function () {
        console.log(this.$el.textContent) // 更新完成
      })
    }
  }
})
</script>

虛擬DOM

Vritual DOM這個概念最先由React引入,是一種DOM對象差異化比較方案,即將DOM對象抽象成為Vritual DOM對象(即render()函數渲染的結果),然后通過差異算法對Vritual DOM進行對比并返回差異,最后通過一個補丁算法將返回的差異對象應用在真實DOM結點。

Vue當中的Virtual DOM對象被稱為VNodetemplate當中的內容會被編譯為render()函數,而render()函數接收一個createElement()函數,并最終返回一個VNode對象),補丁算法來自于另外一個開源項目snabbdom,即將真實的DOM操作映射成對虛擬DOM的操作,通過減少對真實DOM的操作次數來提升性能。

?  vdom git:(dev) tree
├── create-component.js
├── create-element.js
├── create-functional-component.js
├── helpers
│   ├── extract-props.js
│   ├── get-first-component-child.js
│   ├── index.js
│   ├── is-async-placeholder.js
│   ├── merge-hook.js
│   ├── normalize-children.js
│   ├── resolve-async-component.js
│   └── update-listeners.js
├── modules
│   ├── directives.js
│   ├── index.js
│   └── ref.js
├── patch.js
└── vnode.js

VNode的設計出發點與Angular的$digest循環類似,都是通過減少對真實DOM的操作次數來提升性能,但是Vue的實現更加輕量化,摒棄了Angular為了實現雙向綁定而提供的$apply()$eval()封裝函數,有選擇性的實現Angular中$compile()$watch()類似的功能。

Vue對象的選項

通過向構造函數new Vue()傳入一個option對象去創建一個Vue實例。

var vm = new Vue({
  // 數據
  data: "聲明需要響應式綁定的數據對象",
  props: "接收來自父組件的數據",
  propsData: "創建實例時手動傳遞props,方便測試props",
  computed: "計算屬性",
  methods: "定義可以通過vm對象訪問的方法",
  watch: "Vue實例化時會調用$watch()方法遍歷watch對象的每個屬性",
  // DOM
  el: "將頁面上已存在的DOM元素作為Vue實例的掛載目標",
  template: "可以替換掛載元素的字符串模板",
  render: "渲染函數,字符串模板的替代方案",
  renderError: "僅用于開發環境,在render()出現錯誤時,提供另外的渲染輸出",
  // 生命周期鉤子
  beforeCreate: "在Vue實例初始化之后,data observer和event/watcher事件被配置之前",
  created: "發生在Vue實例初始化以及data observer和event/watcher事件被配置之后",
  beforeMount: "掛載開始之前被調用,此時render()首次被調用",
  mounted: "el被新建的vm.$el替換,并掛載到實例上之后調用",
  beforeUpdate: "數據更新前調用,在虛擬DOM重新渲染和打補丁之前觸發",
  updated: "數據更改導致虛擬DOM重新渲染和打補丁之后被調用",
  activated: "keep-alive組件激活時調用",
  deactivated: "keep-alive組件停用時調用",
  beforeDestroy: "實例銷毀之前調用,Vue實例依然可用",
  destroyed: "Vue實例銷毀后調用,事件監聽和子實例全部被移除,釋放系統資源",
  // 資源
  directives: "包含Vue實例可用指令的哈希表",
  filters: "包含Vue實例可用過濾器的哈希表",
  components: "包含Vue實例可用組件的哈希表",
  // 組合
  parent: "指定當前實例的父實例,子實例用this.$parent訪問父實例,父實例通過$children數組訪問子實例",
  mixins: "將屬性混入Vue實例對象,并在Vue自身實例對象的屬性被調用之前得到執行",
  extends: "用于聲明繼承另一個組件,從而無需使用Vue.extend,便于擴展單文件組件",
  provide&inject: "2個屬性需要一起使用,用來向所有子組件注入依賴,類似于React的Context",
  
  // 其它
  name: "允許組件遞歸調用自身,便于調試時顯示更加友好的警告信息",
  delimiters: "改變模板字符串的風格,默認為{{}}",
  functional: "讓組件無狀態(沒有data)和無實例(沒有this上下文)",
  model: "允許自定義組件使用v-model時定制prop和event",
  inheritAttrs: "默認情況下,父作用域的非props屬性綁定會應用在子組件的根元素上。
        當編寫嵌套有其它組件或元素的組件時,可以將該屬性設置為false關閉這些默認行為",
  comments: "設為true時會保留并且渲染模板中的HTML注釋"
});

Vue實例通常使用 vm 變量(View Model)來命名。

屬性計算computed

在HTML模板表達式中放置太多業務邏輯,會讓模板過重且難以維護。因此,可以考慮將模板中比較復雜的表達式拆分到computed屬性當中進行計算。

<!-- 不使用計算屬性 -->
<div id="example">
  {{ message.split("").reverse().join("") }}
</div>

<!-- 將表達式抽象到計算屬性 -->
<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

<script>
  var vm = new Vue({
    el: "#example",
    data: {
      message: "Hello"
    },
    computed: {
      reversedMessage: function () {
        return this.message.split("").reverse().join("")
      }
    }
  })
</script>

計算屬性只在相關依賴發生改變時才會重新求值,這意味只要上面例子中的message沒有發生改變,多次訪問reversedMessage計算屬性總會返回之前的計算結果,而不必再次執行函數,這是computed和method的一個重要區別。

計算屬性默認只擁有getter方法,但是可以自定義一個setter方法。

<script>
... ... ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + " " + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(" ")
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
... ... ...
// 下面語句觸發setter方法,firstName和lastName也會被相應更新
vm.fullName = "John Doe"
</script>

觀察者屬性watch

通過watch屬性可以手動觀察Vue實例上的數據變動,當然也可以調用實例上的vm.$watch達到相同的目的。

<div id="watch-example">
  <p>Ask a yes/no question: <input v-model="question"></p>
  <p>{{ answer }}</p>
</div>

<script>
  var watchExampleVM = new Vue({
    el: "#watch-example",
    data: {
      question: "",
      answer: "I cannot give you an answer until you ask a question!"
    },
    watch: {
      // 如果question發生改變,該函數就會運行
      question: function (newQuestion) {
        this.answer = "Waiting for you to stop typing..."
        this.getAnswer()
      }
    },
    methods: {
      // _.debounce是lodash當中限制操作頻率的函數
      getAnswer: _.debounce(
        function () {
          if (this.question.indexOf("?") === -1) {
            this.answer = "Questions usually contain a question mark. ;-)"
            return
          }
          this.answer = "Thinking..."
          var vm = this
          axios.get("https://yesno.wtf/api")
            .then(function (response) {
              vm.answer = _.capitalize(response.data.answer)
            })
            .catch(function (error) {
              vm.answer = "Error! Could not reach the API. " + error
            })
        },
        // 這是用戶停止輸入等待的毫秒數
        500
      )
    }
  })
</script>

使用watch屬性的靈活性在于,當監測到數據變化的時候,可以做一些設置中間狀態之類的過渡處理。

生命周期

每個Vue實例在創建時,都需要經過一系列初始化過程(設置數據監聽、編譯模板、掛載實例到DOM、在數據變化時更新DOM),并在同時運行一些鉤子函數,讓開發人員能夠在特定生命周期內執行自己的代碼。

image.png

不要在Vue實例的屬性和回調上使用箭頭函數,比如created: () => console.log(this.a)vm.$watch("a", newValue => this.myMethod())。因為箭頭函數的this與父級上下文綁定,并不指向Vue實例本身,所以前面代碼中的this.athis.myMethod將會是undefined

通過jQuery對DOM進行的操作可以放置在Mounted屬性上進行,即當Vue組件已經完成在DOM上掛載的時候。

數據綁定

Vue視圖層通過Mustache["m?st??]語法與Vue實例中的data屬性進行響應式綁定,但是也可以通過內置指令v-once完成一個單向的綁定,再或者通過v-html指令將綁定的字符串輸出為HTML,雖然這樣很容易招受XSS攻擊。

<span>Message: {{ result }}</span>
<span v-once>一次性綁定: {{ msg }}</span>
<div v-html="rawHtml"></div>

Mustache不能用于HTML屬性,此時需要借助于v-bind指令。

<div v-bind:id="dynamicId"></div>
<button v-bind:disabled="isButtonDisabled">Button</button>

綁定HTML的class和style

直接操作classstyle屬性是前端開發當中的常見需求,Vue通過v-bind:classv-bind:style指令有針對性的對這兩種操作進行了增強。

v-bind:class

綁定HTML的class屬性。

<!-- Vue對象中的data -->
<script>
  ... ...
  data: {
    isActive: true,
    hasError: false,
    classObject: {
      active: true,
      "text-danger": false
    }
  }
  ... ...
</script>

<!-- 直接綁定class到一個對象 -->
<div v-bind:class="classObject"></div>

<!-- 直接綁定class到對象的屬性 -->
<div class="static" v-bind:class="{ active: isActive, 
                                text-danger: hasError }"></div>

<!-- 渲染結果 -->
<div class="static active"></div>

可以傳遞一個數組給v-bind:class從而同時設置多個class屬性。

<!-- Vue對象中的data -->
<script>
  ... ...
  data: {
    activeClass: "active",
    errorClass: "text-danger"
  }
  ... ...
</script>

<!-- 綁定class到計算屬性 -->
<div v-bind:class="[activeClass, errorClass]"></div>

<!-- 渲染結果 -->
<div class="active text-danger"></div>

<!-- 使用三目運算符,始終添加errorClass,只在isActive為true時添加activeClass -->
<div v-bind:class="[isActive ? activeClass : "", errorClass]"></div>

<!-- 在數組中使用對象可以避免三目運算符的繁瑣 -->
<div v-bind:class="[{ active: isActive }, errorClass]"></div>

當在自定義組件上使用class屬性時,這些屬性將會被添加到該組件的根元素上面,這一特性同樣適用于v-bind:class

<!-- 聲明一個組件 -->
<script>
  Vue.component("my-component", {
    template: "<p class="foo bar">Hi</p>",
    data: {
      isActive: true
    },
  })
</script>

<!-- 添加2個class屬性 -->
<my-component class="baz boo"></my-component>

<!-- 渲染結果 -->
<p class="foo bar baz boo">Hi</p>

<!-- 使用v-bind:class -->
<my-component v-bind:class="{ active: isActive }"></my-component>

<!-- 渲染結果 -->
<p class="foo bar active">Hi</p>

v-bind:style

綁定HTML的style屬性。

<script>
  ... ...
  data: {
    styleObject: {
      color: "red",
      fontSize: "13px"
    },
    styleHeight: {
      height: 10rem;
    }
    styleWidth: {
      width: 20rem;
    }
  }
  ... ...
</script>

<div v-bind:style="styleObject"></div>

<!-- 使用數組可以將多個樣式合并到一個HTML元素上面 -->
<div v-bind:style="[styleHeight, styleWidth]"></div>

使用v-bind:style時Vue會自動添加prefix前綴,常見的prefix前綴如下:

  • -webkit- Chrome、Safari、新版Opera、所有iOS瀏覽器(包括iOS版Firefox),幾乎所有WebKit內核瀏覽器。
  • -moz- 針對Firefox瀏覽器。
  • -o- 未使用WebKit內核的老版本Opera。
  • -ms- 微軟的IE以及Edge瀏覽器。

使用JavaScript表達式

Vue對于所有數據綁定都提供了JavaScript表達式支持,但是每個綁定只能使用1個表達式。

<span>{{ number + 1 }}</span>
<button>{{ ok ? "YES" : "NO" }}</button>
<p>{{ message.split("").reverse().join("") }}</p>
<div v-bind:id=""list-" + id"></div>

<!-- 這是語句,不是表達式 -->
{{ var a = 1 }}

<!-- if流程控制屬于多個表達式,因此不會生效,但可以使用三元表達式 -->
{{ if (ok) { return message } }}

v-model雙向數據綁定

v-model指令實質上是v-onv-bind的糖衣語法,該指令會接收一個value屬性,存在新值時則觸發一個input事件

<!-- 使用v-model的版本 -->
<input v-model="something">
<!-- 使用v-on和v-bind的版本 -->
<input v-bind:value="something"
       v-on:input="something = $event.target.value">
<!-- 也可以自定義輸入域的響應式綁定 -->
<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>

單選框、復選框一類的輸入域將value屬性作為了其它用途,因此可以通過組件的model選項來避免沖突:

內置指令

帶有v-前綴,當表達式值發生變化時,會響應式的將影響作用于DOM。指令可以接收后面以:表示的參數被指令內部的arg屬性接收),或者以.開頭的修飾符指定該指令以特殊方式綁定)。

<p v-if="seen">Hello world!</p>

<!-- 綁定事件 -->
<a v-bind:href="url"></a>

<!-- 綁定屬性 -->
<a v-on:click="doSomething">

<!-- .prevent修飾符會告訴v-on指令對于觸發的事件調用event.preventDefault() -->
<form v-on:submit.prevent="onSubmit"></form>

Vue為v-bindv-on這兩個常用的指令提供了簡寫形式:@

<!-- v-bind -->
<a v-bind:href="url"></a>
<a :href="url"></a>

<!-- v-on -->
<a v-on:click="doSomething"></a>
<a @click="doSomething"></a>

目前,Vue在2.4.2版本當中提供了如下的內置指令:

<html
  v-text = "更新元素的textContent"
  v-html = "更新元素的innerHTML"
  v-show = "根據表達式的true/false,切換HTML元素的display屬性"
  v-for = "遍歷內部的HTML元素"
  v-pre = "跳過表達式渲染過程,可以顯示原始的Mustache標簽"
  v-cloak = "保持在HTML元素上直到關聯實例結束編譯,可以隱藏未編譯的Mustache"
  v-once = "只渲染元素和組件一次"
></html>

<!-- 根據表達式的true和false來決定是否渲染元素 -->
<div v-if="type === "A"">A</div>
<div v-else-if="type === "B"">B</div>
<div v-else-if="type === "C"">C</div>
<div v-else>Not A/B/C</div>

<!-- 動態地綁定屬性或prop到表達式 -->
<p v-bind:attrOrProp
  .prop = "被用于綁定DOM屬性"
  .camel = "將kebab-case特性名轉換為camelCase"
  .sync = "語法糖,會擴展成一個更新父組件綁定值的v-on監聽器"
></p>

<!-- 綁定事件監聽器 -->
<button
  v-on:eventName
  .stop = "調用event.stopPropagation()"
  .prevent = "調用event.preventDefault()"
  .capture = "添加事件監聽器時使用capture模式"
  .self = "當事件是從監聽器綁定的元素本身觸發時才觸發回調" 
  .native = "監聽組件根元素的原生事件"-
  .once = "只觸發一次回調"
  .left = "點擊鼠標左鍵觸發"
  .right = "點擊鼠標右鍵觸發"
  .middle = "點擊鼠標中鍵觸發"
  .passive = "以{passive: true}模式添加監聽器"
  .{keyCode | keyAlias} = "觸發特定鍵觸事件"
>
</button>

<!-- 表單控件的響應式綁定 -->
<input 
  v-model
  .lazy = "取代input監聽change事件"
  .number = "輸入字符串轉為數字"
  .trim = "過濾輸入的首尾空格" />

組件

組件可以擴展HTML元素功能,并且封裝可重用代碼。可以通過Vue.component( id, [definition] )注冊或者獲取全局組件。

// 注冊組件,傳入一個擴展過的構造器
Vue.component("my-component", Vue.extend({ ... }))

// 注冊組件,傳入一個option對象(會自動調用Vue.extend)
Vue.component("my-component", { ... })

// 獲取注冊的組件(始終返回構造器)
var MyComponent = Vue.component("my-component")

下面代碼創建了一個Vue實例,并將自定義組件my-component掛載至HTML當中。

<script>
  // 注冊自定義組件
  Vue.component("my-component", {
    template: "<div>A custom component!</div>"
  })

  // 創建Vue根實例
  new Vue({
    el: "#example"
  })
</script>

<!-- 原始模板 -->
<div id="example">
  <my-component></my-component>
</div>

<!-- 渲染結果 -->
<div id="example">
  <div>A custom component!</div>
</div>

  • is屬性

瀏覽器解析完HTML之后才會渲染Vue表達式,但是諸如<ul> <ol> <table> <select>限制了可以被包裹的HTML元素,而<option>只能出現在某些HTML元素內部,造成Vue表達式可能不會被正確的渲染。因此,Vue提供is作為屬性別名來解決該問題。

<!-- 不正確的方式 -->
<table>
  <my-row>...</my-row>
</table>

<!-- 使用is的正確方式 -->
<table>
  <tr is="my-row"></tr>
</table>

  • data必須是函數

Vue.component()傳入的data屬性不能是對象,而必須是函數。這樣做的目的是避免組件在相同模板的多個位置被復用時,僅僅返回對象會造成組件間的數據被相互污染,而通過函數每次都返回全新的data對象能完美的規避這個問題。

Vue.component("simple-counter", {
  template: "<button v-on:click="counter += 1">{{ counter }}</button>",
  data: function () {
    return {
      a: "",
      b: ""
    }
  }
});

  • 父子組件之間的通信

父組件通過props向下傳遞數據給子組件,子組件通過events給父組件發送消息,即props 向下傳, events 向上傳

props-events.png

props

雖然每個組件的作用域都是獨立的,但是可以通過props屬性向子組件傳遞數據,這是一種單向數據流的體現形式。

Vue.component("child", {
  // 聲明props
  props: ["message"],
  // 和data屬性一樣,prop也可以在vm通過this.message進行引用
  template: "<span>{{ message }}</span>"
})

不要在子組件內部修改props,這樣會導致后臺報錯。

命名方式轉換

因為HTML并不區分大小寫,所以kebab-case(駝峰)風格命名的props,在組件中會以camelCased(短橫線隔開)風格被接收。

<!-- camelCase in JavaScript -->
<script>
Vue.component("child", {
  props: ["myMessage"],
  template: "<span>{{ myMessage }}</span>"
})
<script>

<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

動態props

可以通過v-bind指令,響應式的綁定父組件數據到子組件的props。當父組件數據變化時,該變化也會傳導至子組件。

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

使用v-bind可以讓其參數值能夠以JavaScript表達式的方式被解析,否則所有傳入的props都會被子組件認為是字符串類型。

<!-- 傳遞的是字符串"1" -->
<comp some-prop="1"></comp>
<!-- 傳遞實際的 number -->
<comp v-bind:some-prop="1"></comp>

驗證props

可以為組件的props指定驗證規則,如果傳入數據不符合要求,Vue會發出相應警告,這樣可以有效提高組件的健壯性。

Vue.component("example", {
  props: {
    // 基礎類型檢測
    propA: Number,
    // 多種類型
    propB: [String, Number],
    // 必傳且是字符串
    propC: {
      type: String,
      required: true
    },
    // 數字,有默認值
    propD: {
      type: Number,
      default: 100
    },
    // 數組或對象的默認值由1個工廠函數返回
    propE: {
      type: Object,
      default: function () {
        return { message: "hello" }
      }
    },
    // 自定義驗證函數
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
});

props會在組件實例創建之前進行校驗。

組件的非props屬性

組件可以接收任意傳入的屬性,這些屬性都會被添加到組件HTML模板的根元素上(無論有沒有在props中定義)。

<!-- 帶有屬性的自定義組件 -->
<bs-date-input
  data-3d-date-picker="true"
  class="date-picker-theme-dark">
</bs-date-input>

<!-- 渲染出來的組件,class屬性被合并 -->
<input type="date" data-3d-date-picker="true" 
      class="form-control date-picker-theme-dark">

父組件傳遞給子組件的屬性可能會覆蓋子組件本身的屬性,因而會對子組件造成破壞和污染。

事件

子組件可以通過Vue的自定義事件與父組件進行通信。

每個Vue實例都實現了如下API,但是并不能直接通過$on監聽子組件冒泡的事件,而必須使用v-on指令。

  1. $on(eventName) 監聽事件
  2. $emit(eventName) 觸發事件

$on$emit并不是addEventListenerdispatchEvent的別名。

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>

<script>
  Vue.component("button-counter", {
    template: "<button v-on:click="incrementCounter">{{counter}}</button>",
    data: function () {
      return {
        counter: 0
      }
    },
    methods: {
      // 子組件事件
      incrementCounter: function () {
        this.counter += 1
        this.$emit("increment") //向父組件冒泡事件
      }
    },
  })

  new Vue({
    el: "#counter-event-example",
    data: {
      total: 0
    },
    methods: {
      // 父組件事件
      incrementTotal: function () {
        this.total += 1
      }
    }
  })
</script>

  • .native修飾符

開發人員也可以在組件的根元素上監聽原生事件,這個時候需要借助到.native修飾符。

<my-component v-on:click.native="doTheThing"></my-component>

  • .sync修飾符

Vue中的props本質是不能進行響應式綁定的,以防止破壞單向數據流,造成多個子組件對父組件狀態形成污染。但是生產環境下,props響應式綁定的需求是切實存在的。因此,Vue將.sync修飾符封裝為糖衣語法,父組件在子組件的props使用該修飾符后,父組件會為props自動綁定v-on事件,子組件則在監聽到props變化時向父組件$emit更新事件,從而讓父組件的props能夠與子組件進行同步。

<!-- 使用.sync修飾符 -->
<comp :foo.sync="bar"></comp>

<!-- 被自動擴展為如下形式,該組件的子組件會通過this.$emit("update:foo", newValue)
    顯式觸發更新事件 -->
<comp :foo="bar" @update:foo="val => bar = val"></comp>

  • 平行組件通信

非父子關系的組件進行通信時,可以使用一個的Vue實例作為中央事件總線

var bus = new Vue()
// 觸發組件A中的事件
bus.$emit("id-selected", 1)
// 在組件B監聽事件
bus.$on("id-selected", function (id) {
  ... ... ...
})

更好的方式是借助VueX或者Redux之類的flux狀態管理庫。

slot

可以將父組件的內容混入到子組件的模板當中,此時可以在子組件中使用<slot>作為父組件內容的插槽。

父組件模板的內容在父組件作用域內編譯,子組件模板的內容在子組件作用域內編譯。

匿名插槽

當子組件只有一個沒有屬性的<slot>時,父組件全部內容片段將插入到插槽所在的DOM位置,并替換插槽標簽本身。

<!-- 子組件my-component的模板 -->
<div>
  <h2>Child</h2>
  <slot>
    父組件沒有需要插入的內容時顯示
  </slot>
</div>

<!-- 父組件模板中使用my-component -->
<div>
  <h1>Parent</h1>
  <child>
    <p>Content 1</p>
    <p>Content 2</p>
  </child>
</div>

<!-- 渲染結果 -->
<div>
  <h1>Parent</h1>
  <div>
    <h2>Child</h2>
    <p>Content 1</p>
    <p>Content 2</p>
  </div>
</div>

<slot>標簽中的內容會在子組件作用域內編譯,并在父組件沒有需要插入的內容時才會顯示。

具名插槽

可以通過<slot>元素的name屬性來配置如何分發內容。

<!-- 子組件 -->
<div id="app">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!-- 父組件 -->
<app>
  <div slot="header">Header</div>
  <p>Content 1</p>
  <p>Content 2</p>
  <div slot="footer">Footer</div>
</app>

<!-- 渲染結果 -->
<div id="app">
  <header>
    <div>Header</div>
  </header>
  <main>
    <p>Content 1</p>
    <p>Content 2</p>
  </main>
  <footer>
    <p>Footer</p>
  </footer>
</div>

匿名slot會作為沒有匹配內容的父組件片段的插槽。

作用域插槽

子組件通過props傳遞數據給<slot>插槽,父組件使用帶有scope屬性的<template>來表示表示當前作用域插槽的模板,scope值對應的變量會接收子組件傳遞來的props對象。

<!-- 子組件通過props傳遞數據給插槽 -->
<div class="child">
  <slot text="hello from child"></slot>
</div>

<!-- 父組件使用帶有scope屬性的<template> -->
<div class="parent">
  <child>
    <template scope="props">
      <span>hello from parent</span>
      <span>{{ props.text }}</span>
    </template>
  </child>
</div>

<!-- 渲染結果 -->
<div class="parent">
  <div class="child">
    <span>hello from parent</span>
    <span>hello from child</span>
  </div>
</div>

函數化組件

即無狀態(沒有data)無實例(沒有this上下文)的組件,渲染開銷較小,且不會出現在Vue devtools當中。

Vue.component("my-component", {
  functional: true,
  // 通過提供context參數為沒有實例的函數組件提供上下文信息
  render: function (createElement, context) {},
  // Props可選
  props: {}
})

動態組件

使用<component>元素并動態綁定其is屬性,可以讓多個組件使用相同的Vue對象掛載點,并實現動態切換。

<script>
var vm = new Vue({
  el: "#example",
  data: {
    currentView: "home"
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
</script>

<component v-bind:is="currentView">
  <!-- 組件在vm.currentview變化時改變! -->
</component>

如果需要將切換的組件保持在內存,保留其狀態并且避免重新渲染,可以使用Vue內置的keep-alive指令。

<keep-alive>
  <component :is="currentView">
    <!-- 非活動組件將被緩存! -->
  </component>
</keep-alive>

組件異步加載

Vue允許將組件定義為工廠函數,從而異步的解析組件定義。Vue只會在組件渲染時才觸發工廠函數,并將結果緩存起來用于后續渲染。定義組件的工廠函數將會接收resolve(接收到從服務器下載的Vue組件options時被調用)和reject(當遠程Vue組件options加載失敗時調用)回調函數作為參數。

Vue.component("async-example", function (resolve, reject) {
  setTimeout(function () {
    // 將組件定義傳遞到resolve回調函數當中
    resolve({
      template: "<div>I am async!</div>"
    })
  }, 1000)
})

可以結合Webpack提供的代碼切割功能,將Vue組件的options對象提取到單獨JavaScript文件,從而實現異步的按需加載。

// 使用webpack的require()來進行異步代碼塊切割
Vue.component("async-webpack-example", function (resolve) {
  require(["./my-async-component"], resolve)
})

// 使用webpack的import()來進行異步代碼塊切割
Vue.component(
  "async-webpack-example", () => import("./my-async-component")
)

從Vue 2.3.0版本開始,可以通過下面的方式來定義一個異步組件。

const AsyncWebpackExample = () => ({
  component: import("./MyComp.vue"),   // 需要加載的組件
  loading: LoadingComp,                // loading時渲染的組件
  error: ErrorComp,                    // 出錯時渲染的組件
  delay: 200,                    // 渲染loading組件前的等待時間(默認:200ms)
  timeout: 3000             // 最長等待時間,超出則渲染error組件(默認:Infinity)
})

在路由組件上使用這種寫法,需要使用vue-router的2.4.0以上版本。

組件的循環引用

循環引用,即兩個組件互相引用對方,例如下面代碼中tree-foldertree-folder-contents兩個組件同時成為了對方的父或子節點,如果使用Webpack模塊化管理工具requiring/importing組件的時候,會報出Failed to mount component: template or render function not defined.錯誤。

<template>
  <p>
    <span>{{ folder.name }}</span>
    <tree-folder-contents :children="folder.children"/>
  </p>
</template>

<template>
  <ul>
    <li v-for="child in children">
      <tree-folder v-if="child.children" :folder="child"/>
      <span v-else>{{ child.name }}</span>
    </li>
  </ul>
</template>

因為tree-foldertree-folder-contents相互引用對方之后,無法確定組件加載的先后順序陷入死循環,所以需要事先指明webpack組件加載的優先級。解決上面例子中Vue組件循環引用的問題,可以在tree-folder組件的beforeCreate()生命周期函數內注冊引發問題的tree-folder-contents組件。

beforeCreate: function () {
  this.$options.components.TreeFolderContents 
      =  require("./tree-folder-contents.vue").default
}

組件命名約定

JavaScript中命名組件組件時可以使用kebab-casecamelCasePascalCase,但HTML模板中只能使用kebab-case格式。

<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<pascal-cased-component></pascal-cased-component>
<!-- 也可以通過自關閉方式使用組件 -->
<kebab-cased-component />

<script>
components: {
  "kebab-cased-component": {},
  "camelCasedComponent": {},
  "PascalCasedComponent": {}
}
</script>

推薦JavaScript中通過PascalCase方式聲明組件, HTML中則通過kebab-case方式使用組件。

組件遞歸

當局部注冊的Vue組件遞歸調用自身時,需要在創建組件時添加name選項,全局注冊的組件則可以省略該屬性,因為Vue會自動進行添加。

// 局部注冊
new Vue({
  el: "#my-component",
  name: "my-component",
  template: "<div><my-component></my-component></div>"
})

// 全局注冊
Vue.component("my-component", {
  // name: "my-component", 可以省略name屬性
  template: "<div><my-component></my-component></div>"
})

組件遞歸出現死循環時,會提示max stack size exceeded錯誤,所以需要確保遞歸操作都擁有一個終止條件(比如使用v-if并返回false)。

組件模板

  • 可以在Vue組件上使用inline-template屬性,組件會將內嵌的HTML內容作為組件本身的模板進行渲染,而非將其作為slot分發的內容。
<my-component inline-template>
  <div>
    <p>These are compiled as the component"s own template.</p>
    <p>Not parent"s transclusion content.</p>
  </div>
</my-component>

  • 也可以通過在<script>標簽內使用type="text/x-template"id屬性來定義一個內嵌模板。
<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>

<script>
Vue.component("hello-world", {
  template: "#hello-world-template"
})
</script>

混合屬性mixins

用來將指定的mixin對象復用到Vue組件當中。

// mixin對象
var mixin = {
  created: function () {
    console.log("混合對象的鉤子被調用")
  },
  methods: {
    foo: function () {
      console.log("foo")
    },
    conflicting: function () {
      console.log("from mixin")
    }
  }
}

// vue屬性
var vm = new Vue({
  mixins: [mixin],
  created: function () {
    console.log("組件鉤子被調用")
  },
  methods: {
    bar: function () {
      console.log("bar")
    },
    conflicting: function () {
      console.log("from self")
    }
  }
})

// => "混合對象的鉤子被調用"
// => "組件鉤子被調用"
vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

同名組件option對象的屬性會被合并為數組依次進行調用,其中mixin對象里的屬性會被首先調用。如果組件option對象的屬性值是一個對象,則mixin中的屬性會被忽略掉。

渲染函數render()

用來創建VNode,該函數接收createElement()方法作為第1個參數,該方法調用后會返回一個虛擬DOM(即VNode)。

直接使用表達式,或者在render()函數內通過createElement()進行手動渲染,Vue都會自動保持blogTitle屬性的響應式更新。

<h1>{{ blogTitle }}</h1>

<script>
  render: function (createElement) {
      return createElement("h1", this.blogTitle)
  }
</script>

如果組件是一個函數組件,render()還會接收一個context參數,以便為沒有實例的函數組件提供上下文信息。

通過render()函數實現虛擬DOM比較麻煩,因此可以使用Babel插件babel-plugin-transform-vue-jsx在render()函數中應用JSX語法。

import AnchoredHeading from "./AnchoredHeading.vue"

new Vue({
  el: "#demo",
  render (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

Vue對象全局API

Vue.extend(options)                 // 通過繼承一個option對象來創建一個Vue實例。
Vue.nextTick([callback, context])   // 在下次DOM更新循環結束之后執行延遲回調。
Vue.set(target, key, value)  // 設置對象的屬性,如果是響應式對象,將會觸發視圖更新。
Vue.delete(target, key)     // 刪除對象的屬性,如果是響應式對象,將會觸發視圖更新。
Vue.directive(id, [definition])     // 注冊或獲取全局指令。
Vue.filter(id, [definition])        // 注冊或獲取全局過濾器。
Vue.component(id, [definition])     // 注冊或獲取全局組件。
Vue.use(plugin)                     // 安裝Vue插件。
Vue.mixin(mixin)                    // 全局注冊一個mixin對象。
Vue.compile(template)               // 在render函數中編譯模板字符串。
Vue.version                         // 提供當前使用Vue的版本號。

Vue.mixin(mixin)

使用全局mixins將會影響到所有之后創建的Vue實例。

// 為自定義選項myOption注入一個處理器。
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: "hello!"
})

// => "hello!"

Vue.directive(id, [definition])

Vue允許注冊自定義指令,用于對底層DOM進行操作。

Vue.directive("focus", {
  bind: function() {
    // 指令第一次綁定到元素時調用,只會調用一次,可以用來執行一些初始化操作。
  },
  inserted: function (el) {
    // 被綁定元素插入父節點時調用。
  },
  update: function() {
    // 所在組件的VNode更新時調用,但是可能發生在其子VNode更新之前。
  },
  componentUpdated: function() {
    // 所在組件VNode及其子VNode全部更新時調用。
  },
  unbind: function() {
    // 指令與元素解綁時調用,只會被調用一次。
  }
})

鉤子之間共享數據可以通過HTMLElementdataset屬性來進行(即HTML標簽上通過data-格式定義的屬性)。

上面的鉤子函數擁有如下參數:

  • el: 指令綁定的HTML元素,可以用來直接操作DOM。
  • vnode: Vue編譯生成的虛擬節點。
  • oldVnode: 之前的虛擬節點,僅在updatecomponentUpdated鉤子中可用。
  • binding: 一個對象,包含以下屬性:
    • name: 指令名稱,不包括v-前綴。
    • value: 指令的綁定值,例如v-my-directive="1 + 1"value的值是2
    • oldValue: 指令綁定的之前一個值,僅在updatecomponentUpdated鉤子中可用。
    • expression: 綁定值的字符串形式,例如v-my-directive="1 + 1"當中expression的值為"1 + 1"
    • arg: 傳給指令的參數,例如v-my-directive:fooarg的值是"foo"
    • modifiers: 包含修飾符的對象,例如v-my-directive.foo.barmodifiers的值是{foo: true, bar: true}

上面參數除el之外,其它參數都應該是只讀的,盡量不要對其進行修改操作。

Vue.filter(id, [definition])

Vue可以通過定義過濾器,進行一些常見的文本格式化,可以用于mustache插值和v-bind表達式當中,使用時通過管道符|添加在表達式尾部。

<!-- in mustaches -->
{{ message | capitalize }}

<!-- in v-bind -->
<div v-bind:id="rawId | formatId"></div>

<!-- capitalize filter -->
<script>
  new Vue({
    filters: {
      capitalize: function (value) {
        if (!value) return ""
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
      }
    }
  })
</script>

過濾器可以串聯使用,也可以傳入參數。

<span>{{ message | filterA | filterB }}</span>
<span>{{ message | filterA("arg1", arg2) }}</span>

Vue.use(plugin)

Vue通過插件來添加一些全局功能,Vue插件都會覆寫其install()方法,該方法第1個參數是Vue構造器, 第2個參數是可選的option對象:

MyPlugin.install = function (Vue, options) {
  // 1\. 添加全局方法或屬性
  Vue.myGlobalMethod = function () {}

  // 2\. 添加全局資源
  Vue.directive("my-directive", {
    bind (el, binding, vnode, oldVnode) {}
  })

  // 3\. 注入組件
  Vue.mixin({
    created: function () {}
  })

  // 4\. 添加實例方法
  Vue.prototype.$myMethod = function (methodOptions) {}
}

通過全局方法Vue.use()使用指定插件,使用的時候也可以傳入一個option對象。

Vue.use(MyPlugin, {someOption: true})

vue-router等插件檢測到Vue是全局對象時會自動調用Vue.use(),如果在CommonJS模塊環境中,則需要顯式調用Vue.use()

實例屬性和方法

Vue實例暴露了一系列帶有前綴$的實例屬性與方法。

let vm = new Vue();
vm = {
  // Vue實例屬性的代理
  $data: "被watch的data對象",
  $props: "當前組件收到的props",
  $el: "Vue實例使用的根DOM元素",
  $options: "當前Vue實例的初始化選項",
  $parent: "父組件Vue對象的實例",
  $root: "根組件Vue對象的實例",
  $children: "當前實例的直接子組件",
  $slots: "訪問被slot分發的內容",
  $scopedSlots: "訪問scoped slots",
  $refs: "包含所有擁有ref注冊的子組件",
  $isServer: "判斷Vue實例是否運行于服務器",
  $attrs: "包含父作用域中非props的屬性綁定",
  $listeners: "包含了父作用域中的v-on事件監聽器",
  // 數據
  $watch: "觀察Vue實例變化的表達式、計算屬性函數",
  $set: "全局Vue.set的別名",
  $delete: "全局Vue.delete的別名",
  // 事件
  $on: "監聽當前實例上的自定義事件,事件可以由vm.$emit觸發",
  $once: "監聽一個自定義事件,觸發一次之后就移除監聽器",
  $off: "移除自定義事件監聽器",
  $emit: "觸發當前實例上的事件",
  // 生命周期
  $mount: "手動地掛載一個沒有掛載的Vue實例",
  $forceUpdate: "強制Vue實例重新渲染,僅影響實例本身和插入插槽內容的子組件",
  $nextTick: "將回調延遲到下次DOM更新循環之后執行",
  $destroy: "完全銷毀一個實例",
}

$refs屬性

組件指定ref屬性之后,可以通過組件的$refs實例屬性對其進行訪問 。

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>

<script>
var parent = new Vue({ el: "#parent" })
var child = parent.$refs.profile // 訪問子組件
</script>

$refs會在組件渲染完畢后填充,是非響應式的,僅作為需要直接訪問子組件的應急方案,因此要避免在模板或計算屬性中使用$refs

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Vue 實例 屬性和方法 每個 Vue 實例都會代理其 data 對象里所有的屬性:var data = { a:...
    云之外閱讀 2,246評論 0 6
  • vue概述 在官方文檔中,有一句話對Vue的定位說的很明確:Vue.js 的核心是一個允許采用簡潔的模板語法來聲明...
    li4065閱讀 7,276評論 0 25
  • 每個 Vue 應用都是通過用 Vue 函數創建一個新的 Vue 實例開始的: 實例生命周期鉤子 每個 Vue 實例...
    Timmy小石匠閱讀 1,394評論 0 11
  • 主要還是自己看的,所有內容來自官方文檔。 介紹 Vue.js 是什么 Vue (讀音 /vju?/,類似于 vie...
    Leonzai閱讀 3,384評論 0 25
  • 早晨醒來,聽到了汀姐在很委屈滴哭,原來是做噩夢了。我一頓心疼,抱著她親了親她的臉頰,告訴依然在睡夢中的她:別怕,媽...
    小妖丁兒閱讀 120評論 0 0