Vue2技術棧歸納與精粹

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

建議暫不具備Vue2開發經驗的同學,完成官方tutorial的學習之後再行閱讀本文。

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

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

因為涉及到的知識點較多,因此本文相對其它技術類文章更長,最好結合markdown書籤閱讀,不過遺憾的是目前知乎暫時未能提供書籤預覽功能,且無法定義子級標題,所以需要使用原文書籤的同學,請移步至本文的Github Pages,結合書籤多級標題能夠更加直觀的了解本文內容,也可以更加方便的定位並閱讀特定章節。

Vue與Angular的比較

為了幫助知乎的小夥伴們更加快速的上手Vue2,特意將這篇文章分享出來,一方面為大家講解Vue2帶來的各種有趣特性,另一方面聊聊與之前使用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()函數重新渲染組件。

與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" // falsevue.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屬性的靈活性在於,當監測到數據變化的時候,可以做一些設置中間狀態之類的過渡處理。

混合屬性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實例在創建時,都需要經過一系列初始化過程(設置數據監聽、編譯模板、掛載實例到DOM、在數據變化時更新DOM),並在同時運行一些鉤子函數,讓開發人員能夠在特定生命周期內執行自己的代碼。

不要在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 down, events up

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>

Vuex狀態管理

Vuex是專門為Vue應用程序提供的狀態管理模式,每個Vuex應用的核心是store倉庫),即裝載應用程序state狀態)的容器,每個應用通常只擁有一個store實例。

Vuex的state是響應式的,即store中的state發生變化時,相應組件也會進行更新,修改store當中state的唯一途徑是提交mutations

const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }})store.commit("increment") // 通過store.state來獲取狀態對象console.log(store.state.count) // 通過store.commit()改變狀態

State

store當中獲取state的最簡單辦法是在計算屬性中返回指定的state,每當state發生改變的時候都會重新執行計算屬性,並且更新關聯的DOM。

const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } }}

Vuex提供store選項,將state從根組件注入到每個子組件中,從而避免頻繁import store

// 父組件中註冊store屬性const app = new Vue({ el: "#app", store: store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div>`})// 子組件,store會注入到子組件,子組件可通過this.$store進行訪問const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } }}

Vuex提供mapState()輔助函數,避免使用多個state的場景下,多次去聲明計算屬性。

// 在單獨構建的版本中輔助函數為 Vuex.mapStateimport { mapState } from "vuex"export default { computed: mapState({ count: state => state.count, // 傳遞字元串參數"count"等同於`state => state.count` countAlias: "count", countPlusLocalState (state) { return state.count + this.localCount } })}// 當計算屬性名稱與state子節點名稱相同時,可以向mapState傳遞一個字元串數組computed: mapState([ "count" // 映射this.count到store.state.count])

mapState()函數返回一個包含有state相關計算屬性的對象,這裡可以通過ES6的對象展開運算符...將該對象與Vue組件本身的computed屬性進行合併。

computed: { localComputed () {}, ...mapState({})}

Vuex允許在store中定義getters可視為store的計算屬性),getters的返回值會根據其依賴被緩存,只有當依賴值發生了改變才會被重新計算。該方法接收state作為第1個參數,其它getters作為第2個參數。可以直接在store上調用getters來獲取指定的計算值。

const store = new Vuex.Store({ state: { todos: [ { id: 1, text: "...", done: true }, { id: 2, text: "...", done: false } ] }, getters: { doneTodos: (state, getters) => { return state.todos.filter(todo => todo.done) } }})// 獲取doneTodos = [{ id: 1, text: "...", done: true }]store.getters.doneTodos

這樣就可以方便的根據store中現有的state派生出新的state,從而避免在多個組件中復用時造成代碼冗餘。

computed: { doneTodosCount () { return this.$store.getters.doneTodos // 現在可以方便的在Vue組件使用store中定義的doneTodos }}

Vuex提供的mapGetters()輔助函數將store中的getters映射到局部計算屬性。

import { mapGetters } from "vuex"export default { computed: { // 使用對象展開運算符將getters混入computed計算屬性 ...mapGetters([ "doneTodosCount", doneCount: "doneTodosCount" // 映射store.getters.doneTodosCount到別名this.doneCount ]) }}

Mutations

修改store中的state的唯一方法是提交mutation([mju?」te??(?)n] n.變化),mutations類似於自定義事件,擁有一個字元串事件類型和一個回調函數(接收state作為參數,是對state進行修改的位置)。

const store = new Vuex.Store({ state: { count: 1 }, mutations: { // 觸發類型為increment的mutation時被調用 increment (state) { state.count++ // 變更狀態 } }})// 觸發mutationstore.commit("increment")

可以通過store的commit()方法觸髮指定的mutations,也可以通過store.commit()向mutation傳遞參數。

// commit()store.commit({ type: "increment", amount: 10})// storemutations: { increment (state, payload) { state.count += payload.amount }}

mutation事件類型建議使用常量,並且將這些常量放置在單獨文件,便於管理和防止重複。

// mutation-types.jsexport const SOME_MUTATION = "SOME_MUTATION"// store.jsimport Vuex from "vuex"import { SOME_MUTATION } from "./mutation-types"const store = new Vuex.Store({ state: { ... }, mutations: { // 可以通過ES6的計算屬性命名特性去使用常量作為函數名 [SOME_MUTATION] (state) { // mutate state } }})

mutation()必須是同步函數,因為devtool無法追蹤回調函數中對state進行的非同步修改。

Vue組件可以使用this.$store.commit("xxx")提交mutation,或者使用mapMutations()將Vue組件中的methods映射為store.commit調用(需要在根節點注入store)。

import { mapMutations } from "vuex"export default { methods: { ...mapMutations([ "increment" // 映射this.increment()為this.$store.commit("increment") ]), ...mapMutations({ add: "increment" // 映射this.add()為this.$store.commit("increment") }) }}

Actions

Action用來提交mutation,且Action中可以包含非同步操作。Action函數接受一個與store實例具有相同方法和屬性的context對象,因此可以通過調用context.commit提交一個mutation,或者通過context.statecontext.getters來獲取state、getters。

const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit("increment") } }})

生產環境下,可以通過ES6的解構參數來簡化代碼。

actions: { // 直接向action傳遞commit方法 increment ({ commit }) { commit("increment") }}

Action通過store.dispatch()方法進行分發,mutation當中只能進行同步操作,而action內部可以進行非同步的操作。下面是一個購物車的例子,代碼中分發了多個mutations,並進行了非同步API操作。

actions: { checkout ({ commit, state }, products) { const savedCartItems = [...state.cart.added] // 把當前購物車的物品備份起來 commit(types.CHECKOUT_REQUEST) // 發出結賬請求,然後清空購物車 // 購物Promise分別接收成功和失敗的回調 shop.buyProducts( products, () => commit(types.CHECKOUT_SUCCESS), // 成功操作 () => commit(types.CHECKOUT_FAILURE, savedCartItems) // 失敗操作 ) }}

組件中可以使用this.$store.dispatch("xxx")分發action,或者使用mapActions()將組件的methods映射為store.dispatch需要在根節點注入store)。

import { mapActions } from "vuex"export default { methods: { ...mapActions([ "increment" // 映射this.increment()為this.$store.dispatch("increment") ]), ...mapActions({ add: "increment" // 映射this.add()為this.$store.dispatch("increment") }) }}

store.dispatch可以處理action回調函數當中返回的Promise,並且store.dispatch本身仍然返回一個Promise

actions: { // 定義一個返回Promise對象的actionA actionA ({ commit }) { return new Promise((resolve, reject) => { setTimeout(() => { commit("someMutation") // 觸發mutation resolve() }, 1000) }) }, // 也可以在actionB中分發actionA actionB ({ dispatch, commit }) { return dispatch("actionA").then(() => { commit("someOtherMutation") // 觸發另外一個mutation }) }}// 現在可以分發actionAstore.dispatch("actionA").then(() => { ... ... ...})

可以體驗通過ES7的非同步處理特性async/await來組合action。

actions: { async actionA ({ commit }) { commit("gotData", await getData()) }, async actionB ({ dispatch, commit }) { await dispatch("actionA") //等待actionA完成 commit("gotOtherData", await getOtherData()) }}

Module

整個應用使用單一狀態樹的情況下,所有state都會集中到一個store對象,因此store可能變得非常臃腫。因此,Vuex允許將store切割成模塊(module),每個模塊擁有自己的statemutationactiongetter、甚至是嵌套的子模塊。

const moduleA = { state: {}, mutations: {}, actions: {}, getters: {}}const moduleB = { state: {}, mutations: {}, actions: {}}const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB }})store.state.a // moduleA的狀態store.state.b // moduleB的狀態

module內部的mutations()getters()接收的第1個參數是模塊的局部狀態對象。

const moduleA = { state: { count: 0 }, mutations: { increment (state) { state.count++ // 這裡的state是模塊的局部狀態 } }, getters: { doubleCount (state) { return state.count * 2 } }}

模塊內部action當中,可以通過context.state獲取局部狀態,以及context.rootState獲取全局狀態。

const moduleA = { // ... actions: { incrementIfOddOnRootSum ({ state, commit, rootState }) { if ((state.count + rootState.count) % 2 === 1) { commit("increment") } } }}

模塊內部的getters()方法,可以通過其第3個參數接收到全局狀態。

const moduleA = { getters: { sumWithRootCount (state, getters, rootState) { return state.count + rootState.count } }}

嚴格模式

嚴格模式下,如果state變化不是由mutation()函數引起,將會拋出錯誤。只需要在創建store的時候傳入strict: true即可開啟嚴格模式。

const store = new Vuex.Store({ strict: true})

不要在生產環境下啟用嚴格模式,因為它會深度檢測不合法的state變化,從而造成不必要的性能損失,我們可以通過在構建工具中增加如下判斷避免這種情況。

const store = new Vuex.Store({ strict: process.env.NODE_ENV !== "production"})

嚴格模式下,在屬於Vuex的state上使用v-model指令會拋出錯誤,此時需要手動綁定value並監聽input、change事件,並在事件回調中手動提交action。另外一種實現方式是直接重寫計算屬性的get和set方法。

總結

  1. 應用層級的狀態應該集中到單個store對象中。
  2. 提交mutation是更改狀態的唯一方法,並且這個過程是同步的。
  3. 非同步邏輯都應該封裝到action裡面。

Webpack Vue Loader

vue-loader是由Vue開源社區提供的Webpack載入器,用來將.vue後綴的單文件組件轉換為JavaScript模塊,每個.vue單文件組件可以包括以下部分:

  1. 一個<template>
  2. 一個<script>
  3. 多個<style>

<template>只能有1個</template><script>只能有1個</script><style>可以有多個</style><style>可以有多個</style><style>可以有多個</style>

CSS作用域

.vue單文件組件的<style>標籤上添加scoped屬性,可以讓該<style>標籤中的樣式只作用於當前組件。使用scoped時,樣式選擇器盡量使用class或者id,以提升頁面渲染性能。

<!-- 單文件組件定義 --><style scoped>.example { color: red;}</style><template> <div class="example">Hank</div></template><!-- 轉換結果 --><style>.example[data-v-f3f3eg9] { color: blue;}</style><template> <div class="example" data-v-f3f3eg9>Hank</div></template>

可以在一個組件中同時使用帶scoped屬性和不帶該屬性的<style/>,分別用來定義組件私有樣式和全局樣式。

CSS模塊化

在單文件組件.vue<style>標籤上添加module屬性即可打開CSS模塊化特性。CSS Modules用於模塊化組合CSS,vue-loader已經集成了CSS模塊化特性。

<style module>.red { color: red;}.bold { font-weight: bold;}</style>

CSS模塊會向Vue組件中注入名為$style計算屬性,從而實現在組件的<template/>中使用動態的class屬性進行綁定。

<template> <p :class="$style.red"> This should be red </p></template>

動畫

Vue在插入、更新、移除DOM的時候,提供了如下幾種方式去展現進入(entering)和離開(leaving)的過渡效果。

  1. 在CSS過渡和動畫中應用class。
  2. 鉤子過渡函數中直接操作DOM。
  3. 使用CSS、JavaScript動畫庫,如Animate.css、Velocity.js。

transition組件

Vue提供了內置組件<transition/>來為HTML元素、Vue組件添加過渡動畫效果,可以在條件展示使用v-ifv-show)、動態組件展示組件根節點的情況下進行渲染。<transition/>主要用來處理單個節點,或者同時渲染多個節點當中的一個。

自動切換的class類名

在組件或HTML進入(entering)和離開(leaving)的過渡效果當中,Vue將會自動切換並應用下圖中的六種class類名。

可以使用<transition/>name屬性來自動生成過渡class類名,例如下面例子中的name: "fade"將自動拓展為.fade-enter.fade-enter-active等,name屬性預設的情況下默認類名為v

<div id="demo"> <button v-on:click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition></div><script>new Vue({ el: "#demo", data: { show: true }})</script><style>.fade-enter-active, .fade-leave-active { transition: opacity .5s}.fade-enter, .fade-leave-to { opacity: 0}</style>

自定義CSS類名

結合Animate.css使用時,可以在<transition/>當中通過以下屬性自定義class類名。

<transition enter-class = "animated" enter-active-class = "animated" enter-to-class = "animated" leave-class = "animated" leave-active-class = "animated" leave-to-class = "animated"></transition>

自定義JavaScript鉤子

結合Velocity.js使用時,通過v-on在屬性中設置鉤子函數。

<transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:after-enter="afterEnter" v-on:enter-cancelled="enterCancelled" v-on:before-leave="beforeLeave" v-on:leave="leave" v-on:after-leave="afterLeave" v-on:leave-cancelled="leaveCancelled"></transition><script>// ...methods: { beforeEnter: function (el) {}, enter: function (el, done) { done() }, afterEnter: function (el) {}, enterCancelled: function (el) {}, beforeLeave: function (el) {}, leave: function (el, done) { done() }, afterLeave: function (el) {}, leaveCancelled: function (el) {} // 僅用於v-show}</script>

顯式設置過渡持續時間

可以使用<transition>上的duration屬性設置一個以毫秒為單位的顯式過渡持續時間。

<transition :duration="1000"> Hank </transition><!-- 可以分別定製進入、移出的持續時間 --><transition :duration="{ enter: 500, leave: 800 }"> Hank </transition>

組件首次渲染時的過渡

通過<transition>上的appear屬性設置組件節點首次被渲染時的過渡動畫。

<!-- 自定義CSS類名 --><transition appear appear-class="custom-appear-class" appear-to-class="custom-appear-to-class" appear-active-class="custom-appear-active-class"></transition><!-- 自定義JavaScript鉤子 --><transition appear v-on:before-appear="customBeforeAppearHook" v-on:appear="customAppearHook" v-on:after-appear="customAfterAppearHook" v-on:appear-cancelled="customAppearCancelledHook"></transition>

HTML元素的過渡效果

Vue組件的key屬性

key屬性主要用在Vue虛擬DOM演算法中去區分新舊VNodes,不顯式使用key的時候,Vue會使用性能最優的自動比較演算法。顯式的使用key,則會基於key的變化重新排列元素順序,並移除不存在key的元素。具有相同父元素的子元素必須有獨特的key,因為重複的key會造成渲染錯誤。

<ul> <!-- 最常見的用法是在使用v-for的時候 --> <li v-for="item in items" :key="item.id">...</li></ul>

元素的的交替過渡

可以通過Vue提供的v-ifv-else屬性來實現多組件的交替過渡,最常見的過渡效果是一個列表以及描述列表為空時的消息。

<transition> <table v-if="items.length > 0"> <!-- ... --> </table> <p v-else>Sorry, no items found.</p></transition>

Vue中具有相同名稱的元素切換時,需要通過關鍵字key作為標記進行區分,否則Vue出於效率的考慮只會替換相同標籤內的內容,因此為<transition>組件中的同名元素設置key是一個最佳實踐

<transition> <button v-if="isEditing" key="save"> Save </button> <button v-else key="edit"> Edit </button></transition>

一些場景中,可以通過給相同HTML元素的key屬性設置不同的狀態來代替冗長的v-ifv-else

<!-- 通過v-if和v-else來實現 --><transition> <button v-if="isEditing" key="save"> Save </button> <button v-else key="edit"> Edit </button></transition><!-- 設置動態的key屬性來實現 --><transition> <button v-bind:key="isEditing"> {{ isEditing ? "Save" : "Edit" }} </button></transition>

而對於使用了多個v-if的多元素過渡,也可以通過動態的key屬性進行大幅度的簡化。

<!-- 多個v-if實現的多元素過渡 --><transition> <button v-if="docState === "saved"" key="saved"> Edit </button> <button v-if="docState === "edited"" key="edited"> Save </button> <button v-if="docState === "editing"" key="editing"> Cancel </button></transition><!-- 通過動態key屬性可以大幅簡化模板代碼 --><transition> <button v-bind:key="docState"> {{ buttonMessage }} </button></transition><script>...computed: { buttonMessage: function () { switch (this.docState) { case "saved": return "Edit" case "edited": return "Save" case "editing": return "Cancel" } }}</script>

Vue組件的過渡效果

多個Vue組件之間的過渡不需要使用key屬性,只需要使用動態組件即可。

<transition name="component-fade" mode="out-in"> <component v-bind:is="view"></component></transition><script>new Vue({ el: "#transition-components-demo", data: { view: "v-a" }, components: { "v-a": { template: "<div>Component A</div>" }, "v-b": { template: "<div>Component B</div>" } }})<script><style>.component-fade-enter-active, .component-fade-leave-active { transition: opacity .3s ease;}.component-fade-enter, .component-fade-leave-to { opacity: 0;}<style>

選擇HTML元素或Vue組件的過渡模式

<transition>的默認進入(enter)和離開(leave)行為同時發生,所以當多個需要切換顯示的HTML元素或Vue組件處於相同位置的時候,這種同時生效的進入和離開過渡不能滿足所有需求,Vue可以通過<transition-gruop>組件的mode屬性來選擇如下過渡模式。

  • in-out:新元素先進行過渡,完成之後當前顯示的元素再過渡離開。
  • out-in:當前顯示的元素先進行過渡,完成之後新元素再過渡進入。

<transition name="fade" mode="out-in"> <button v-if="docState === "saved"" key="saved"> Edit </button> <button v-if="docState === "edited"" key="edited"> Save </button> <button v-if="docState === "editing"" key="editing"> Cancel </button></transition>

transition-group組件

<transition-group>用來設置多個HTML元素或Vue組件的過渡效果,不同於<transition>,該組件默認會被渲染為一個真實的<span>元素,但是開發人員也可以通過<transition-group>組件的tag屬性更換為其它合法的HTML元素。<transition-group>組件內部的元素必須要提供唯一的key屬性值。

<div id="list-demo" class="demo"> <button v-on:click="add">Add</button> <button v-on:click="remove">Remove</button> <transition-group name="list" tag="p"> <span v-for="item in items" v-bind:key="item" class="list-item"> {{ item }} </span> </transition-group></div><script>new Vue({ el: "#list-demo", data: { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) }, }})</script><style>.list-item { display: inline-block; margin-right: 10px;}.list-enter-active, .list-leave-active { transition: all 1s;}.list-enter, .list-leave-to { opacity: 0; transform: translateY(30px);}</style>

<transition-group>實現的列表過渡效果在添加、移除某個HTML元素時,相臨的其它HTML元素會瞬間移動至新位置,這個過程並非平滑的過渡。為解決這個問題,<transition-group>提供v-move特性去覆蓋移動過渡期間所使用的CSS類名。開啟該特性,即可以通過name屬性手動設置(下面例子中的name="flip-list".flip-list-move),也可以直接使用move-class屬性。

<div id="flip-list-demo" class="demo"> <button v-on:click="shuffle">Shuffle</button> <transition-group name="flip-list" tag="ul"> <li v-for="item in items" v-bind:key="item"> {{ item }} </li> </transition-group></div><script>new Vue({ el: "#flip-list-demo", data: { items: [1,2,3,4,5,6,7,8,9] }, methods: { shuffle: function () { this.items = _.shuffle(this.items) } }})</script><style>.flip-list-move { transition: transform 1s;}<style>

可以通過響應式的綁定<transition><transition-gruop>上的name屬性,從而能夠根據組件自身的狀態實現具有動態性的過渡效果。

<transition v-bind:name="transitionName"></transition>


筆者就職於天府軟體園C區的IT企業,親身經歷成都市南沿線的快速城市化進程,以及成都IT產業的迅速聚集與崛起。為了方便生活和工作在城南同學們的溝通與交流,所以在知乎上按照天府軟體園建設的順序,依建立了A、B、C、D四個QQ群,目前ABC三個500人群都已經滿員,並且不再接受新成員加入,所以請新加入的同學移步至D區群621843391,關於成都IT產業的討論依然如故,風格依然有趣 (^_^)。

同時歡迎大家關注知乎專欄《成都IT圈》,專欄將集中成都市IT產業趨勢、城市經濟發展思考、IT技術交流方面的話題與文章,也歡迎大家踴躍向專欄進行投稿。

本群為嚴肅群,彙集的都是素質比較高同學,拒絕惡趣味,新加入的同學請嚴格按照群通知要求修改群名片,本群定期清理長期不修改的同學。

推薦閱讀:

2.2 webpack
React把PropTypes放到一個獨立包
Rx 的編程方式(一)
前端 UI組件化的一些思考

TAG:Vuejs | React | JavaScript |